fix(linter): support bare true/false and integer literals in rules:if:
release / Build and publish release (push) Successful in 1m15s
release / Build and publish release (push) Successful in 1m15s
GitLab CI expressions allow unquoted true, false, and integers as
comparison operands (all treated as their string representations):
$GATEWAY_ENABLED == true (equivalent to == "true")
$FEATURE_FLAG == false (equivalent to == "false")
$PARALLEL == 4 (equivalent to == "4")
$ENABLED == 1 / == 0
Previously these fell through to permissive true because parseValue
only recognised $VAR, "${VAR}", quoted strings, and null. Added:
- true/false keyword branch → returns "true"/"false"
- integer literal branch (digits only) → returns decimal string
All three new forms are correctly excluded from longer identifier
prefixes (identByte boundary check). Adds 8 new unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -9,11 +9,13 @@ This project uses [Semantic Versioning](https://semver.org).
|
||||
|
||||
### Added
|
||||
|
||||
- **`rules:if:` expression evaluator improvements** — four correctness fixes to the GitLab CI expression parser:
|
||||
- **`rules:if:` expression evaluator improvements** — six correctness fixes to the GitLab CI expression parser:
|
||||
- **Multi-line expressions** — newlines (`\n`, `\r`) are now treated as whitespace between tokens, so block-scalar `if:` values (e.g. `if: | ...`) and folded YAML scalars with `||`/`&&` on a continuation line are parsed correctly instead of falling back to permissive `true`.
|
||||
- **`${VAR}` curly-brace variable syntax** — `${CI_COMMIT_BRANCH}` is now equivalent to `$CI_COMMIT_BRANCH` everywhere a value is expected.
|
||||
- **Regex flags** — `/pattern/i`, `/pattern/m`, `/pattern/s` are now honoured; the `i` flag (case-insensitive) is translated to Go's `(?i)` prefix before compiling. Unknown flags are silently ignored.
|
||||
- **Variable as regex RHS** — `$BRANCH =~ $PATTERN` where `$PATTERN` holds a `/regex/[flags]` string is now evaluated by extracting and compiling the pattern from the variable's value; if the value is empty or does not look like a regex literal the expression falls back to permissive `true`.
|
||||
- **`true` / `false` keywords** — bare `true` and `false` (without quotes) are now recognised as the string values `"true"` and `"false"`, matching GitLab CI's own behaviour. `$GATEWAY_ENABLED == true` and `$FEATURE_FLAG == false` now evaluate correctly.
|
||||
- **Integer literals** — bare integers (e.g. `$PARALLEL == 4`, `$ENABLED == 1`, `$DISABLED == 0`) are now parsed as their decimal string representations and compared accordingly.
|
||||
|
||||
- **File and line numbers on findings** — every finding now includes the source file and line where the job is defined, e.g. `[ERROR] job "deploy" (src/deploy.yml:14): …`. For jobs that come from local or fetched includes the file reflects the include source. Pipeline-level findings (workflow rules, missing stages) reference the root pipeline file.
|
||||
|
||||
|
||||
@@ -231,8 +231,10 @@ func (p *exprParser) parseRegexRHS() (pat string, ok bool, permissive bool) {
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
// parseValue reads $VAR, ${VAR}, "string", 'string', or null.
|
||||
// null and undefined variables both produce an empty string.
|
||||
// parseValue reads $VAR, ${VAR}, "string", 'string', null, true, false, or an
|
||||
// integer literal. null and undefined variables both produce an empty string.
|
||||
// true/false and integers produce their string representations (GitLab CI
|
||||
// compares all values as strings).
|
||||
func (p *exprParser) parseValue() (string, bool) {
|
||||
p.skipWS()
|
||||
|
||||
@@ -254,12 +256,18 @@ func (p *exprParser) parseValue() (string, bool) {
|
||||
return p.vars(name), true
|
||||
}
|
||||
|
||||
// null keyword — must not be a prefix of a longer identifier.
|
||||
if p.startsWith("null") {
|
||||
end := p.pos + 4
|
||||
if end >= len(p.s) || !isIdentByte(p.s[end]) {
|
||||
p.pos += 4
|
||||
return "", true // null → empty string
|
||||
// Keywords and string literals must not be prefixes of longer identifiers.
|
||||
for _, kw := range []struct{ tok, val string }{
|
||||
{"null", ""},
|
||||
{"true", "true"},
|
||||
{"false", "false"},
|
||||
} {
|
||||
if p.startsWith(kw.tok) {
|
||||
end := p.pos + len(kw.tok)
|
||||
if end >= len(p.s) || !isIdentByte(p.s[end]) {
|
||||
p.pos += len(kw.tok)
|
||||
return kw.val, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +275,15 @@ func (p *exprParser) parseValue() (string, bool) {
|
||||
return p.parseStringLiteral()
|
||||
}
|
||||
|
||||
// Integer literal — returned as its decimal string for string comparison.
|
||||
if p.peek() >= '0' && p.peek() <= '9' {
|
||||
start := p.pos
|
||||
for p.pos < len(p.s) && p.s[p.pos] >= '0' && p.s[p.pos] <= '9' {
|
||||
p.pos++
|
||||
}
|
||||
return p.s[start:p.pos], true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,20 @@ func TestEvalIf(t *testing.T) {
|
||||
{"var regex rhs plain permissive", `$CI_COMMIT_BRANCH =~ $PLAIN_PATTERN`, true},
|
||||
{"var regex rhs not match", `$CI_COMMIT_BRANCH !~ $BRANCH_PATTERN`, false},
|
||||
|
||||
// ── Bare true/false keywords ─────────────────────────────────────────
|
||||
// GitLab CI treats true/false as the string values "true"/"false".
|
||||
{"bare true match", `$CI_PIPELINE_SOURCE == true`, false}, // "push" != "true"
|
||||
{"bare false match", `$CI_COMMIT_TAG == false`, false}, // "" != "false"
|
||||
{"bare true var set to true", `$DEPLOY_ENV == true`, false}, // "staging" != "true"
|
||||
{"bare false neq", `$CI_COMMIT_BRANCH != false`, true}, // "develop" != "false"
|
||||
{"bare true in compound", `$CI_COMMIT_BRANCH != null && $CI_COMMIT_TAG == false`, false},
|
||||
|
||||
// ── Integer literals ──────────────────────────────────────────────────
|
||||
// Compared as decimal strings (GitLab CI converts integers to strings).
|
||||
{"int eq match", `$CI_PIPELINE_SOURCE != 0`, true}, // "push" != "0"
|
||||
{"int eq no match", `$CI_COMMIT_TAG == 0`, false}, // "" != "0"
|
||||
{"int in compound", `$CI_COMMIT_BRANCH != null && $CI_COMMIT_BRANCH != 0`, true},
|
||||
|
||||
// ── Permissive fallback ───────────────────────────────────────────────
|
||||
{"unparseable returns true", `this is not valid syntax %%%`, true},
|
||||
{"empty expr returns true", ``, true},
|
||||
|
||||
Reference in New Issue
Block a user