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
|
### 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`.
|
- **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.
|
- **`${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.
|
- **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`.
|
- **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.
|
- **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
|
return "", false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseValue reads $VAR, ${VAR}, "string", 'string', or null.
|
// parseValue reads $VAR, ${VAR}, "string", 'string', null, true, false, or an
|
||||||
// null and undefined variables both produce an empty string.
|
// 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) {
|
func (p *exprParser) parseValue() (string, bool) {
|
||||||
p.skipWS()
|
p.skipWS()
|
||||||
|
|
||||||
@@ -254,12 +256,18 @@ func (p *exprParser) parseValue() (string, bool) {
|
|||||||
return p.vars(name), true
|
return p.vars(name), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// null keyword — must not be a prefix of a longer identifier.
|
// Keywords and string literals must not be prefixes of longer identifiers.
|
||||||
if p.startsWith("null") {
|
for _, kw := range []struct{ tok, val string }{
|
||||||
end := p.pos + 4
|
{"null", ""},
|
||||||
if end >= len(p.s) || !isIdentByte(p.s[end]) {
|
{"true", "true"},
|
||||||
p.pos += 4
|
{"false", "false"},
|
||||||
return "", true // null → empty string
|
} {
|
||||||
|
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()
|
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
|
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 plain permissive", `$CI_COMMIT_BRANCH =~ $PLAIN_PATTERN`, true},
|
||||||
{"var regex rhs not match", `$CI_COMMIT_BRANCH !~ $BRANCH_PATTERN`, false},
|
{"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 ───────────────────────────────────────────────
|
// ── Permissive fallback ───────────────────────────────────────────────
|
||||||
{"unparseable returns true", `this is not valid syntax %%%`, true},
|
{"unparseable returns true", `this is not valid syntax %%%`, true},
|
||||||
{"empty expr returns true", ``, true},
|
{"empty expr returns true", ``, true},
|
||||||
|
|||||||
Reference in New Issue
Block a user