SIGN IN SIGN UP

fix: jq keyword tokenization for field access (dot adjacency) (#101)

* test: add failing tests for keyword-named field access in jq

The jq tokenizer unconditionally maps identifiers like 'label', 'and',
'or', 'not', 'if', 'try', 'catch', 'reduce', 'foreach', 'def', and
'break' to their keyword token types, even when they appear after a dot
as field accessors. The parser's parsePrimary and parsePostfix methods
only accept IDENT or STRING tokens after a dot, so expressions like
.label, .and, or .data.label fail to parse as field access.

In real jq, these are valid field accesses — 'label' is only a keyword
in the context of 'label $out | ... | break $out', not after a dot.

These 13 tests confirm the bug across all affected keywords and chained
field access scenarios. All tests currently fail.

Amp-Thread-ID: https://ampcode.com/threads/T-019c319a-3aa1-73e7-99bf-c63c261bf5d7
Co-authored-by: Amp <amp@ampcode.com>

* fix(jq): treat keyword-named identifiers as field names after dot

The parser rejected field access on keys named after keywords (e.g.
.label, .and, .def) because the tokenizer emitted keyword token types
and the parser only accepted IDENT or STRING after a dot.

Fix by using token position adjacency to disambiguate: .label (no space)
is field access, while . or (with space) remains identity + operator,
matching real jq behavior.

Changes:
- Store lexeme as value on keyword tokens so field names are recoverable
- Add KEYWORD_TOKEN_TYPES set and isAdjacentFieldName() parser helper
- Update parsePrimary and parsePostfix to accept adjacent keywords as
  field names after dot

Amp-Thread-ID: https://ampcode.com/threads/T-019c319a-3aa1-73e7-99bf-c63c261bf5d7
Co-authored-by: Amp <amp@ampcode.com>

* fix(query-engine): accept keywords as object keys and deduplicate field name parsing

- Extract consumeFieldNameAfterDot() and isIdentLike() helpers to
  eliminate duplicated adjacency check logic between parsePrimary and
  parsePostfix
- Update parseObjectConstruction to accept keyword tokens (label, not,
  and, etc.) as bare object keys, fixing {label: .label} and {label}
- Update parsePatternField to accept keyword tokens as destructuring
  pattern keys, fixing `. as {label: $l}`
- Remove now-passing "Keywords as object keys" skip from jq spec tests

* jq: rename isAdjacentFieldName and add negative adjacency tests

Rename isAdjacentFieldName to isFieldNameAfterDot to better reflect
that identifiers and strings are accepted regardless of adjacency,
while only keywords require strict adjacency to the dot.

Add tests verifying that '. if', '. and', and '. try' (with space)
produce errors rather than being treated as field access.

* fix(jq): require adjacency for identifiers after dot, matching real jq

Real jq requires dot-adjacent identifiers and keywords for field access
(`.foo`, `.if`) but allows whitespace before strings (`."foo"`, `. "foo"`).

Previously, identifiers were accepted regardless of whitespace after dot,
meaning `. foo` would incorrectly parse as field access instead of erroring.

- Require adjacency (pos === dot.pos + 1) for IDENT tokens, same as keywords
- Keep STRING tokens allowed with whitespace, matching real jq behavior
- Update both isFieldNameAfterDot() and consumeFieldNameAfterDot()
- Add comprehensive dot-adjacency tests covering keywords (null, true, false,
  then, else, end, as, etc.), identifiers, strings, chained access, double
  spaces, and postfix contexts (after .[0], after parens)

---------

Co-authored-by: Ryan Skoblenick <1390364+skoblenick@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
R
Ryan Skoblenick committed
db62a85015a43588bb3e098de984d0909c1a6f1e
Parent: 0d87a8f
Committed by GitHub <noreply@github.com> on 2/7/2026, 3:57:37 AM