Fix anyOf/oneOf completion narrowing when a branch is clean-by-omission (#307)#338
Open
bryceschober wants to merge 2 commits into
Open
Fix anyOf/oneOf completion narrowing when a branch is clean-by-omission (#307)#338bryceschober wants to merge 2 commits into
bryceschober wants to merge 2 commits into
Conversation
anyOf/oneOf branch completion incorrectly narrows discriminator values when one variant requires additional properties that haven't been typed yet. These tests reproduce the bug reported in microsoft#307 for both forms: - a plain anyOf (matching the shape from the original issue report) - oneOf with an explicit "discriminator" keyword (the pattern real-world discriminated-union schemas, e.g. those emitted by Pydantic/OpenAPI generators, commonly use instead of anyOf) Both assert an empty/partial discriminator value offers all valid branch values, and an exact match still narrows to one. This commit intentionally contains only the failing tests, without the fix, to document the bug in isolation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
getMatchingSchemas excludes the AST node currently being typed from validation during value completion, so an anyOf/oneOf branch that requires an additional not-yet-typed property can appear "problem-free" purely because there was nothing to check yet, not because it's a genuinely better structural match. testAlternatives' winner-selection logic then discarded the sibling branch entirely, causing IntelliSense to only suggest one discriminator value instead of all valid ones. Add hasExclusion() to ISchemaCollector so testAlternatives can detect this exclusion-driven case. When evaluating alternatives during value completion (collector has an active exclude node) and the "clean" alternative has zero propertiesValueMatches (nothing about it was substantively confirmed by real data), merge both alternatives' matching schemas instead of letting one fully eliminate the other. This applies equally to anyOf and oneOf: the exclusion-driven inconclusiveness affects both the same way during completion, and doesn't affect oneOf's real "exactly one must match" diagnostic, which never runs against an excluded node. Real-world discriminated-union schemas (e.g. those emitted by Pydantic/OpenAPI generators) commonly use oneOf with a discriminator keyword rather than plain anyOf, so both forms need this fix. This distinguishes the microsoft#307 scenario (clean only by omission) from legitimate narrowing based on already-typed sibling data, which existing tests (e.g. 'Complete with oneOf and enums') continue to rely on. Fixes microsoft#307 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Author
|
@microsoft-github-policy-service agree company="Dynon Avionics" |
bryceschober
marked this pull request as ready for review
July 10, 2026 19:02
Author
|
@aeschli I'm not sure what it would take to get attention for this kind of PR. Can you offer any guidance regarding how I could better help? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #307
Summary
IntelliSense value completion for a discriminator property inside
anyOf/oneOfover-narrows when one branch has additional required properties that haven't been typed yet — only one valid discriminator value is suggested instead of all of them.This happens because
getMatchingSchemasexcludes the AST node currently being typed from validation during completion, so a branch can look "problem-free" purely because there was nothing to check yet, not because it's a genuinely better structural match.testAlternatives' winner-selection then fully discards the sibling branch.Disclaimer
This may not be the best path to the desired behavior. This path was chosen by CoPilot under direction from a SW engineer unfamiliar with Typescript, this module, and the VS Code architecture.
Fix
hasExclusion()toISchemaCollectorto detect when we're doing value completion (anexcludenode is set) rather than diagnostics.testAlternatives, when one alternative's "problem-free" status is inconclusive (no problems, but nothing substantive matched either) while the other has real problems, both alternatives' matching schemas are merged instead of letting one fully eliminate the other.anyOfandoneOf(includingoneOf+ an explicitdiscriminatorkeyword, common in real-world discriminated-union schemas from tools like Pydantic/OpenAPI generators) — this doesn't affectoneOf's real "exactly one must match" diagnostic, which never runs against an excluded node.'Complete with oneOf and enums'test) is unaffected.Tests
Two regression tests added in their own commit (separate from the fix), covering the
anyOfandoneOf+discriminatorshapes respectively.Verification
npm run compileclean,node --test→ 4716 passed / 0 failed / 16 skipped,npm run lintclean.oneOf+discriminatoracross dozens of variants.Process notes for maintainers (optional reading)
The notes below document how this fix was derived, for additional context if useful during review. Not required reading.
Problem (#307)
Value completion for a discriminator-like property inside
anyOf(and, as later discovered,oneOfwith adiscriminatormapping) was over-narrowed when one branch has additional required properties the user hasn't typed yet. Completing a"type"(or similarly-named) discriminator property suggested only one valid value instead of all of them, while the object was still incomplete.Root cause
getMatchingSchemasexcludes the AST node currently being typed from validation during completion, so the node's own incomplete value never counts against it. IntestAlternatives(theanyOf/oneOfbranch evaluator injsonParser.ts), this can make a branch look "problem-free" purely because there was nothing to check yet — not because it's a genuinely better structural match. Thecompare()-based winner-selection logic then fully discarded the sibling branch, even though it's still legitimately reachable.A separate existing test ("Complete with oneOf and enums") has a similar-looking asymmetric-required-properties shape where full elimination IS correct — there, the elimination is backed by real, already-typed sibling data, not just an artifact of exclusion. The fix has to distinguish these two cases.
Fix
Implemented in
src/parser/jsonParser.ts:hasExclusion()toISchemaCollector— true only when the collector was constructed with anexcludenode, i.e. during value completion (never during diagnostics/validate()).testAlternatives, when the collector has an active exclusion and one alternative's problem-free status is "inconclusive" (no problems, but also zero real matched data backing it) while the other alternative has real problems, both alternatives' matching schemas are merged instead of letting one fully discard the other.anyOfonly (!maxOneMatch); generalized to also coveroneOfafter discovering that real-world discriminated-union schemas (produced by tools like Pydantic/OpenAPI code generators) commonly useoneOf+ an explicitdiscriminatorkeyword rather than plainanyOf. The same clean-by-omission bug reproduced identically there. The existing "inconclusive clean match" guard already protects legitimateoneOfnarrowing regardless ofmaxOneMatch, so removing that gate was safe.jsonCompletion.tsinstead (a blanket fallback showing all discriminator values whenever the current value was empty). It fixed IntelliSense incorrectly narrows anyOf branches when one variant requires additional properties #307 but broke the legitimate "Complete with oneOf and enums" narrowing test, so it was reverted in favor of the scopedjsonParser.tsfix above.Tests
Two regression tests added to
src/test/completion.test.ts, split into their own commit (separate from the fix commit):anyOfwith two object branches sharing a discriminator property, one branch requiring an extra property — mirrors the schema shape from the original issue report.oneOf+$defs/$ref+ an explicitdiscriminator(propertyName/mapping) keyword — mirrors real-world discriminated-union schemas encountered while validating the fix against a private production codebase.Both assert that an empty/partial discriminator value offers all valid branch values, and that an exact match still narrows to one (existing behavior preserved).
Verification
git stash) and passes after the fix.npm run compileclean;node --test→ 4716 passed / 0 failed / 16 skipped (includes the official JSON Schema Test Suite'soneOf/anyOfconformance tests);npm run lintclean.npm linkto a checkout ofmicrosoft/vscode'sjson-language-featuresextension, against a private production schema unrelated to this repo that usesoneOf+discriminatoracross dozens of variants. Confirmed previously-hidden discriminator values now appear during completion for both a top-level command list and a nested object-type list.Status
Fix and both regression tests are complete, verified, and pushed as two commits to the contributor's fork branch (tests-only commit, then fix-only commit). PR opened in draft against
microsoft/vscode-json-languageservicereferencing #307.Contribution guidelines checklist (from Microsoft/vscode wiki "How to Contribute")
Fixes #307.anyOffix and itsoneOfgeneralization share the same root cause (clean-by-omission during exclusion), so combining them in one PR is appropriate.