Files
yq/pkg/yqlib/expression_parser_test.go
4d5f0bda59 feat: add jq style if-then-elif-else-end conditionals (#2871)
* feat: add jq style if-then-elif-else-end conditionals

Adds `if A then B elif C then D else E end`, following jq semantics:

- the condition is evaluated against each input, using the same
  truthiness as select / and / or (null and false are falsy)
- each result of the condition produces a result
- `else` is optional; when missing, falsy inputs are returned unchanged
  (jq 1.7 behaviour)
- a condition with no results (e.g. a missing key) is treated as null,
  consistent with `and`, `or` and `//`

`if` / `end` are lexed as brackets so the conditional can be followed by
traversals and used anywhere an expression can. `then` / `elif` / `else`
are binary ops with the lowest precedence, and the postfixer checks that
the keywords are in the right order inside an if block. Path elements like
`.if.then.else.end` are unaffected.

Closes #95

* updating tools

* build(deps): bump golang from 1.26.6 to 1.27.1 (#2855)

Bumps golang from 1.26.6 to 1.27.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.24 (#2866)

Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.24.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.24)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/text from 0.41.0 to 0.42.0 (#2867)

Bumps [golang.org/x/text](https://github.com/golang/text) from 0.41.0 to 0.42.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/mod from 0.40.0 to 0.41.0 (#2858)

Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/mod/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(toml): quote empty keys (#2870)

* build(deps): bump alpine from `28bd5fe` to `294b683` (#2875)

Bumps alpine from `28bd5fe` to `294b683`.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang from `512690a` to `3680233` (#2876)

Bumps golang from `512690a` to `3680233`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump golang.org/x/net from 0.58.0 to 0.59.0 (#2868)

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.58.0 to 0.59.0.
- [Commits](https://github.com/golang/net/compare/v0.58.0...v0.59.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/hashicorp/hcl/v2 from 2.24.0 to 2.25.0 (#2878)

Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.24.0 to 2.25.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.24.0...v2.25.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hcl/v2
  dependency-version: 2.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github.com/magiconair/properties (#2877)

Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.18.11 to 1.18.12.
- [Release notes](https://github.com/magiconair/properties/releases)
- [Commits](https://github.com/magiconair/properties/compare/v1.18.11...v1.18.12)

---
updated-dependencies:
- dependency-name: github.com/magiconair/properties
  dependency-version: 1.18.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: preserve explicit assignments in read-only expressions (#2850)

* fix: preserve explicit assignments in read-only expressions

AI agent disclosure: This commit was prepared by Codex acting on RRXXZZYY's behalf.

* fix: allow attribute assignments to create paths

---------

Co-authored-by: RRXXZZYY <[email protected]>

* Fixing assignment with merge (#2883)

* Writable context for compound assignments

* Fixing assignment with merge

* test: cover explicit YAML null conditions in if-then-else

Add a documented scenario showing that empty, ~, null and Null values
are falsy, plus tests for a tagged !!null, a null condition with no
else, and the remaining truthiness cases (only null and false are
falsy).

* Extracting if logic from expression_postfix

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Mike Farah <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jake Wang <[email protected]>
Co-authored-by: ZhongYu Li <[email protected]>
Co-authored-by: RRXXZZYY <[email protected]>
2026-09-29 13:43:28 +10:00

154 lines
5.6 KiB
Go

package yqlib
import (
"testing"
"github.com/mikefarah/yq/v4/test"
)
func getExpressionParser() ExpressionParserInterface {
InitExpressionParser()
return ExpressionParser
}
func TestParserCreateMapColonOnItsOwn(t *testing.T) {
_, err := getExpressionParser().ParseExpression(":")
test.AssertResultComplex(t, "':' expects 2 args but there is 0", err.Error())
}
func TestParserNoMatchingCloseBracket(t *testing.T) {
_, err := getExpressionParser().ParseExpression(".cat | with(.;.bob")
test.AssertResultComplex(t, "bad expression - probably missing close bracket on WITH", err.Error())
}
func TestParserNoMatchingCloseCollect(t *testing.T) {
_, err := getExpressionParser().ParseExpression("[1,2")
test.AssertResultComplex(t, "bad expression, could not find matching `]`", err.Error())
}
func TestParserNoMatchingCloseObjectInCollect(t *testing.T) {
_, err := getExpressionParser().ParseExpression(`[{"b": "c"]`)
test.AssertResultComplex(t, "bad expression, could not find matching `}`", err.Error())
}
func TestParserNoMatchingCloseInCollect(t *testing.T) {
_, err := getExpressionParser().ParseExpression(`[(.a]`)
test.AssertResultComplex(t, "bad expression, could not find matching `)`", err.Error())
}
func TestParserNoMatchingCloseCollectObject(t *testing.T) {
_, err := getExpressionParser().ParseExpression(`{"a": "b"`)
test.AssertResultComplex(t, "bad expression, could not find matching `}`", err.Error())
}
func TestParserNoMatchingCloseCollectInCollectObject(t *testing.T) {
_, err := getExpressionParser().ParseExpression(`{"b": [1}`)
test.AssertResultComplex(t, "bad expression, could not find matching `]`", err.Error())
}
func TestParserNoMatchingCloseBracketInCollectObject(t *testing.T) {
_, err := getExpressionParser().ParseExpression(`{"b": (1}`)
test.AssertResultComplex(t, "bad expression, could not find matching `)`", err.Error())
}
func TestParserNoArgsForTwoArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression("=")
test.AssertResultComplex(t, "'=' expects 2 args but there is 0", err.Error())
}
func TestParserOneLhsArgsForTwoArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression(".a =")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
}
func TestParserOneRhsArgsForTwoArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression("= .a")
test.AssertResultComplex(t, "'=' expects 2 args but there is 1", err.Error())
}
func TestParserTwoArgsForTwoArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression(".a = .b")
test.AssertResultComplex(t, nil, err)
}
func TestParserNoArgsForOneArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression("explode")
test.AssertResultComplex(t, "'explode' expects 1 arg but received none", err.Error())
}
func TestParserOneArgForOneArgOp(t *testing.T) {
_, err := getExpressionParser().ParseExpression("explode(.)")
test.AssertResultComplex(t, nil, err)
}
func TestParserExtraArgs(t *testing.T) {
_, err := getExpressionParser().ParseExpression("sortKeys(.) explode(.)")
test.AssertResultComplex(t, "bad expression, please check expression syntax", err.Error())
}
func TestParserEmptyExpression(t *testing.T) {
_, err := getExpressionParser().ParseExpression("")
test.AssertResultComplex(t, nil, err)
}
func TestParserSingleOperation(t *testing.T) {
result, err := getExpressionParser().ParseExpression(".")
test.AssertResultComplex(t, nil, err)
if result == nil {
t.Fatal("Expected non-nil result for single operation")
return
}
if result.Operation == nil {
t.Fatal("Expected operation to be set")
}
}
func TestParserFirstOpWithZeroArgs(t *testing.T) {
// Test the special case where firstOpType can accept zero args
result, err := getExpressionParser().ParseExpression("first")
test.AssertResultComplex(t, nil, err)
if result == nil {
t.Fatal("Expected non-nil result for first operation with zero args")
}
}
func TestParserInvalidExpressionTree(t *testing.T) {
// This tests the createExpressionTree function with malformed postfix
parser := getExpressionParser().(*expressionParserImpl)
// Create invalid postfix operations that would leave more than one item on stack
invalidOps := []*Operation{
{OperationType: &operationType{NumArgs: 0}},
{OperationType: &operationType{NumArgs: 0}},
}
_, err := parser.createExpressionTree(invalidOps)
test.AssertResultComplex(t, "bad expression, please check expression syntax", err.Error())
}
func TestParserIfThenElseErrors(t *testing.T) {
var scenarios = []struct {
expression string
expectedError string
}{
{`if . then 1`, "bad expression, could not find matching `end`"},
{`if . then 1 )`, "bad expression, could not find matching `end`"},
{`[if . then 1]`, "bad expression, could not find matching `end`"},
{`(. + 1 end`, "bad expression, got `end` without matching `if`"},
{`. then 1`, "bad expression, `then` without matching `if`"},
{`if (. then 1) end`, "bad expression, `then` without matching `if`"},
{`if . then 1 then 2 end`, "bad expression, `then` must follow `if` or `elif`"},
{`if . else 1 end`, "bad expression, `else` must follow `then`"},
{`if . then 1 else 2 elif 3 then 4 end`, "bad expression, `elif` must follow `then`"},
{`if . end`, "bad expression, `if` must be followed by `then`"},
{`if . then 1 elif 2 end`, "bad expression, `elif` must be followed by `then`"},
}
for _, s := range scenarios {
_, err := getExpressionParser().ParseExpression(s.expression)
if err == nil {
t.Errorf("expected error for %v", s.expression)
continue
}
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.expression)
}
}