Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new rule to check if null checking is being done using short circuit #238

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

bmbferreira
Copy link

This adds a new rule terraform_no_short_circuit_evaluation that checks for attempts to use logical operators (&&, ||) with null checks that could lead to errors due to lack of short-circuit evaluation.

This is one of the most common mistakes I usually see in Terraform for beginners. While this issue was recently fixed in Terraform core (hashicorp/terraform#24128), older Terraform versions don't support short-circuit evaluation. Therefore, it would be helpful to have TFLint check this optionally by enabling this rule.

The rule will warn about code patterns like:

# This will error if var.obj is null
count = var.obj != null && var.obj.enabled ? 1 : 0
# Should be written as:
count = var.obj != null ? var.obj.enabled ? 1 : 0 : 0

The rule could be enabled by default and included in the "recommended" preset since this is a common source of errors for Terraform users.

@bmbferreira bmbferreira requested a review from wata727 February 9, 2025 00:38
@wata727
Copy link
Member

wata727 commented Feb 9, 2025

Nice! This is one of the surprising behaviors of the Terraform language, and it's great to be able to find the possible error statically.
It's probably impossible to find all issues, but it's a good idea to narrow the scope to common patterns.

Furthermore, it would be nice to be able to auto-fix the issue.

I'll review this again later.

// Try to get variable name from LHS
if scopeTraversalExpr, ok := binaryOpExpr.LHS.(*hclsyntax.ScopeTraversalExpr); ok {
if len(scopeTraversalExpr.Traversal) > 0 {
varName = scopeTraversalExpr.Traversal.RootName()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RootName will only return var for addresses like var.foo. So there are false positives in expressions like:

var.foo != null && var.bar ? 1 : 0


// referencesNullCheckedVar checks if the right side expression references the same variable that was null checked
func referencesNullCheckedVar(nullCheck, expr hcl.Expression) bool {
// Get the variable name from the null check
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of searching for the variable reference again in referencesNullCheckedVar, it would be better for isNullCheck to return it.

// isNullCheck determines if an expression is checking for null
func isNullCheck(expr hcl.Expression) bool {
if binaryOpExpr, ok := expr.(*hclsyntax.BinaryOpExpr); ok {
if binaryOpExpr.Op == hclsyntax.OpEqual || binaryOpExpr.Op == hclsyntax.OpNotEqual {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, the only valid combinations are OpLogicalAnd with OpNotEqual and OpLogicalOr with OpEqual. However, in practice, an expression like var.obj == null && var.obj.enabled is rarely useful, so this may be sufficient as it is.

if referencesNullCheckedVar(binaryOpExpr.LHS, binaryOpExpr.RHS) {
if err := runner.EmitIssue(
r,
"Short-circuit evaluation is not supported in Terraform. Use a conditional expression (condition ? true : false) instead.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's debatable whether it's appropriate to always suggest a conditional expression as a fix. According to hashicorp/terraform#24128 there are a variety of different solutions.
As you already mentioned in the documentation, another option is to use the try function.

@@ -22,6 +22,7 @@ var PresetRules = map[string][]tflint.Rule{
NewTerraformUnusedDeclarationsRule(),
NewTerraformUnusedRequiredProvidersRule(),
NewTerraformWorkspaceRemoteRule(),
NewTerraformNoShortCircuitEvaluationRule(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this rule should be added to the recommended preset.

Copy link
Member

@bendrucker bendrucker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an upcoming version of Terraform eliminates this footgun that's a great reason for this not to be enabled by default. Users of Terraform who are sufficiently up to date should be free to use the more familiar/concise syntax. Authors of reusable modules need to be more concerned about backward compatibility.

@@ -0,0 +1,60 @@
# terraform_no_short_circuit_evaluation
Copy link
Member

@bendrucker bendrucker Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

terraform_logical_operator_short_circuit would be a better name. Most of the other rules are anti-patterns but none uses a no prefix.

@@ -0,0 +1,60 @@
# terraform_no_short_circuit_evaluation

Disallow using logical operators (`&&`, `||`) with null checks that could lead to errors due to lack of short-circuit evaluation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Disallow using logical operators (`&&`, `||`) with null checks that could lead to errors due to lack of short-circuit evaluation.
Detects usage of logical operators (`&&`, `||`) that could lead to errors because of their lack of support for short-circuit evaluation.

Doesn't seem like null is an essential part of the description, you might be looking at length or any other intended pre-condition.


Unlike many programming languages, Terraform's logical operators (`&&` and `||`) do not short-circuit. This means that in an expression like `var.obj != null && var.obj.enabled`, both sides will be evaluated even if `var.obj` is null, which will result in an error.

This is a common source of confusion for users coming from other programming languages where short-circuit evaluation is standard behavior. The issue is particularly problematic when checking for null before accessing object attributes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

particularly problematic

Compared to what?


Use nested conditional expressions instead of logical operators when you need short-circuit behavior. For example:

```hcl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split this into multiple code blocks for readability

try(var.obj.enabled, false)
```

For more information, see [hashicorp/terraform#24128](https://github.com/hashicorp/terraform/issues/24128).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too open-ended for users, too much history to sort through. Instead link to the official docs on this limitation:

https://developer.hashicorp.com/terraform/language/expressions/operators#logical-operators

And then perhaps summarize any version-specific context, e.g. if it only affects Terraform versions earlier than x.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants