Skip to content

jq: a slice write re-reads a target that stopped being null, but reports the wrong refusal #2927

Description

@newhoggy

Severity: Low (message wording on a write that fails either way; found by /code-review on PR #2926)

Summary

jq's INDEX re-reads the target's kind at write time, so a slice component whose bound
it never parsed reports a kind-dependent error. succinctly's through_slice refuses such a
component before looking at root, so it always reports the bound error.

Only reachable when a slot stops being null between path resolution and the write — i.e.
an earlier path in the same fan-out replaced it.

Repro

Verified live against /usr/bin/jq 1.7.1 on main @ PR #2926's branch:

$ echo '{"a":null}' | jq -c '(.a, .a["x":]) = 5'
jq: error (at <stdin>:1): Cannot index number with object

$ echo '{"a":null}' | succinctly jq -c '(.a, .a["x":]) = 5'
jq: error (at <stdin>:1): Array/string slice indices must be integers

Same for |= 5. Both tools error and both exit 5; only the sentence differs.

Not a regression

Before #2926 the resolver raised Array/string slice indices must be integers for this
same program, at resolution rather than at the write — so the observable behaviour is
unchanged. #2926 moved where the refusal happens without changing what it says.

Root cause

through_slice (src/jq/eval.rs) checks SliceEditFlags::non_integer_bound ahead of its
match root, because that flag is a property of the component rather than of the target.
The component can only be produced over a null target (#2853), but by the time the
write runs the slot may hold something else.

Why the obvious narrowing is wrong

Gating the arm on matches!(root, OwnedValue::Null) and letting everything else fall
through would be worse than a wrong sentence. A SliceBoundKey::Raw bound carries
start: None, end: None, which every arm below reads as the whole slice — so an array
target would be silently spliced rather than refused. Any fix has to answer the
kind-dependent error explicitly, not delegate to the existing arms.

Suggested fix direction

Give the arm a kind-aware refusal, mirroring SliceTargetKind's existing ordering (#2546):
array/string target → Array/string slice indices must be integers; anything else →
Cannot index <kind> with object. Capture the full oracle matrix first (number, string,
array, object, boolean, and the ?/|=/del spellings of each) — the del() row
especially, since delpaths never parses the descriptor and may well no-op regardless of
kind.

Why this is separate from #2853

#2853 is about a path that resolves where succinctly used to raise. This is about a
target whose kind changed underneath an already-resolved path, which is a different
mechanism, has its own oracle matrix, and was not a divergence #2853 introduced.

Refs #2853, #2546, #2926.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    SonnetSuitable for a Sonnet-class model to implement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions