prefer-string-interpolation
Summary: Prefer string interpolation
Category: Idiomatic
Avoid
package policy
request_formatted := sprintf("%s: %s", [method, path]) if {
# extract method and path from the request
}
full_name := concat(" ", [first_name, last_name])
Prefer
package policy
request_formatted := $"{method}: ${path}" if {
# extract method and path from the request
}
full_name := $"{first_name} ${last_name}"
Rationale
The introduction of string interpolation
in OPA (v1.12.0) is one of the most significant
improvements to the Rego language since OPA 1.0, and interpolation should now be preferred over alternative methods,
like concat and sprintf, whenever possible.
String interpolation improves readability by placing variables, references and even whole expressions directly in the string to be rendered. Coupled with sensible variable names, the result is code that is easier to both read and write, while losing none of its expressiveness. String interpolation comes with many additional benefits, but also a few caveats to be aware of.
Handling of Undefined
String interpolation additionally simplifies rendering strings that contain references to potentially undefined
documents. Undefined references would typically cause evaluation to halt if provided as arguments to built-in functions
like concat or sprintf. While this is consistent with how Rego normally evaluates undefined, some scenarios benefit
from a more lenient approach, where for example rendering a message as part of a decision ideally doesn't impact the
decision itself, even if the message resolves a reference to an undefined document. Consider for example a rule building
a set of "deny" messages using the sprintf function:
package policy
deny contains message if {
#
# [ any number of conditions ]
#
# rendering message string fails on missing input attributes — may impact decision
message := sprintf("user %s missing required attributes", [input.user.name])
}
Even with all conditions in the rule body above being met, the set returned by evaluating the deny rule could
still end up empty (typically interpreted as "not deny") in the case where user.name is missing from the input
document. This is unlikely what the policy author intended! While Rego provides many ways to help address this, adding
additional code just to render a message string is often undesirable. OPA's string interpolation feature was designed
with this in mind, and renders references to undefined documents simply as <undefined> without causing evaluation to
halt.
package policy
deny contains message if {
#
# [ any number of conditions ]
#
# rendering message string never fails — no impact on decision
message := $"user ${input.user.name} missing required attributes"
}
While the more lenient handling of undefined references commonly is desired when rendering strings, this may not always
be the case. Since replacing a call to concat or sprintf with string interpolation potentially changes the outcome
of evaluation, Regal by default only reports call-sites where all arguments to the function are guaranteed to be defined,
as is the case of e.g. local variables or function arguments, but not nested references to attributes of the data or
input documents. See Configuration Options if you want Regal to also consider non-variable
arguments passed to concat or sprintf, with the mentioned caveat in mind.
Portability
As a built-in language feature, string interpolation additionally provides improved portability across different OPA and
Rego implementations, where sprintf in particular may not be available, or only partially supported. As noted under
Exceptions below, sprintf may still be required for more advanced string formatting needs. Regal
identifies such cases and will only report use of formatting functions replaceable by string interpolation.
Exceptions
While we consider string interpolation to be the idiomatic option over alternatives like concat or sprintf whenever
applicable, it's simplicity means it won't handle all the advanced formatting options that sprintf provides. Regal
currently recognizes and reports only cases of sprintf use where the following conditions are met:
- No verbs besides
%s,%q,%dor%vare used in the format string - The arguments array contains only references to variables known to be defined at evaluation time, or Regal is configured to also consider calls with non-variable arguments (see Configuration Options below)
Regal will suggest replacing most uses of the concat built-in function with string interpolation, applying the same
default for handling non-variable arguments as calls to sprintf.
We recommend setting include-non-var-args: true in your configuration only in combination with manual review of any
calls rewritten to use interpolation, and that you have tests to ensure the correctness of the result.
Configuration Options
This linter rule provides the following configuration options:
rules:
idiomatic:
prefer-string-interpolation:
# one of "error", "warning", "ignore"
level: error
# whether to include non-variable arguments such as references to
# `data` or `input` or the result of evaluated expressions passed
# to `sprintf`. For example, this rule would by default disqualify the
# a call like `sprintf("%s:%d", [x, input.y])` since `input.y` could be
# undefined at evaluation time and changing the call to use interpolation
# would potentially change the outcome of evaluation
#
# while this is often acceptable, Regal ultimately leaves that decision
# up to the user.
#
# default: false
include-non-var-args: true
Related Resources
- OPA Docs: String Interpolation
- OPA Docs: sprintf
- GitHub: Source Code