fix(linter): support bare true/false and integer literals in rules:if:
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:
2026-06-11 22:20:59 +02:00
parent e931b9d1c9
commit a0e2582cf1
3 changed files with 42 additions and 9 deletions
+25 -8
View 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
}