mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* feat(ingestion): tree-sitter node-type/field validation gate + remove dead literals
Add a CI gate (test/integration/grammar-literal-validation.test.ts) validating
every node-type and field-name literal in the ingestion code layer against each
grammar's node-types.json, with a live `new Parser.Query` probe fallback for
literals the static JSON under-reports. Covers all three surfaces:
- legacy Call-Resolution DAG (type-extractors, *-extractors/configs) + the
ungated structure phase (field/method extractors, export-detection) — AST scan;
- registry scope-resolution captures + scope queries (Mode 3 compile);
- the registry RESOLUTION layer (scope-resolver/type-binding/receiver-binding/
interpret/arity/import-decomposer …) via a TS-TypeChecker discriminator that
collects a literal ONLY when its `.type` receiver is a tree-sitter SyntaxNode
(so resolved-symbol `.type` kinds like 'Class' are never mistaken for nodes).
Helpers: test/helpers/{grammar-introspection,literal-collectors}.ts.
Remove every existence-dead literal the gate surfaces (behavior-neutral
dead-branch/fallback deletions verified absent from the installed grammar),
spanning the legacy, structure-phase, and registry production paths:
reference_type/pointer_type/scoped_identifier/scoped_type_identifier/
rvalue_reference_declarator/variadic_parameter (C/C++), equals_value_clause/
identifier_name/simple_identifier/record_struct_declaration/record_class_declaration
(C#), generic_type/`type` field (Dart), nullable_type (PHP), method_call/symbol
(Ruby), method_call_expression/slice_type/shorthand_field_pattern (Rust),
struct_declaration/internal_name (Swift), comment (Java), parameter/
parameterized_type and dead childForFieldName('pattern'|'modifiers'|
'formal_parameters'|'declaration'|'default'|'return_value'|'alias_clause') /
class_expression fallbacks. Gate ships with an empty allowlist.
One behavior FIX (scope-resolution): PHP `findEnclosingTypeDeclaration` omitted
`anonymous_class`, so a method inside an anonymous class mis-bound `$this` to the
enclosing named class; add `anonymous_class` so it is correctly skipped.
Verified: tsc clean; gate green (empty allowlist); scope-resolution parity 26/26
on both REGISTRY_PRIMARY_*=0 and =1; resolver suite no new failures.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ingestion): assert real grammar node types in #1920 dead-literal tests
Three tests asserted defensive handling of node types the installed
grammars never emit (verified via real tree-sitter parse), so they broke
once the dead literals were removed in af9d709f:
- parsing.test.ts isNodeExported / csharp: `record struct` and `record
class` both parse to `record_declaration` (kept in CSHARP_DECL_TYPES) —
tree-sitter-c-sharp emits no `record_struct_declaration` /
`record_class_declaration` node. Switch the two mock nodes to
`record_declaration`.
- extract-generic-type-args.test.ts: Java emits `generic_type` and Kotlin
`user_type`+`type_projection`; `parameterized_type` is produced by no
installed grammar, so the shared extractor returns [] for it. Convert the
case to a documented negative assertion (real paths already covered by the
generic_type cases).
No source behavior change: production export detection (record_declaration)
and generic type-arg extraction (generic_type / type_projection) were
already correct. Fixes the 3 CI failures on PR #1937.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ingestion): keep parameterized_type generic-arg extraction (allowlisted)
Restore the `parameterized_type` branch in extractSimpleTypeName /
extractGenericTypeArgs (type-extractors/shared.ts) so a parameterized_type
node still yields its type arguments (List<User> -> [User]). Current
tree-sitter-java emits `generic_type` and tree-sitter-kotlin
`user_type`+`type_projection`, so this is a defensive alternate node kept
for grammar-version resilience; it is allowlisted in the node-type
validation gate with a documented justification rather than removed.
extract-generic-type-args.test.ts now asserts the User type argument is
captured from a parameterized_type node.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ingestion): extract generic args from real grammar nodes, drop parameterized_type guess
extractGenericTypeArgs / extractSimpleTypeName special-cased `parameterized_type`,
a node type NO installed grammar emits (real parse: Java/TypeScript/Rust ->
generic_type, C# -> generic_name, Kotlin -> user_type). It was a guess masking a
real gap; remove it.
The genuine 'Kotlin alternate node type' is `user_type` (`List<User>` parses to
user_type > [type_identifier, type_arguments]), which the extractor returned []
for. Handle it: read a user_type's own type_arguments, else recurse into its
wrapped child (preserving the existing user_type > generic_type unwrap). No
production caller passes user_type today (Kotlin generics resolve via jvm.ts), so
this only makes the function's documented Kotlin contract correct — zero
behaviour change for current callers (Java/TS/C#/Rust pass generic_type/name).
Replace the mock parameterized_type test with REAL-PARSE coverage across
Java/TypeScript/C#/Rust/Kotlin (+ Java Map<String,User>) so a wrong node-type
guess can't silently pass again. Gate allowlist returns to empty.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(test): wrap real-parse cases to prettier printWidth (CI format gate)
CI runs `prettier --check .` from the repo root (printWidth 100) and flagged the
new real-parse cases array's long single-line object literals. Wrap them.
Format-only; no behaviour change.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ingestion): node-scoped field probe oracle for the literal gate (U1)
Add probeField(language, nodeType, field) — the node-scoped analogue of
probeNodeType: compiles `(<nodeType> <field>: (_)) @_` against the live
grammar and classifies TSQueryErrorStructure/Field -> dead, TSQueryErrorNodeType
(node absent here) -> unavailable, compile -> valid. Conservative-toward-valid
(supertype-typed fields make some wrong fields compile), so it never produces a
false positive. Add isFieldError classifier; make validateField's node-scoped
path membership-then-probe so node-types.json field under-reporting can't yield
a false `dead`.
Foundation for the node-scoped field validation gate (no gate behavior change
yet). Issue #1920 (epic #1919).
* test(ingestion): capture receiver node type + extend Mode-4 to type-env.ts (U2)
CollectedField gains receiverNodeType, captured conservatively by
receiverNodeTypeOf: only when a childForFieldName receiver is unambiguously
narrowed by a single enclosing positive guard (if (recv.type==='X') then-branch,
or switch case 'X') with no reassignment/shadowing of the receiver in the
enclosing function. Any uncertainty -> undefined (sound global fallback);
fail-safe (benign false negative, never a false positive).
Extend Mode-4's resolutionLayerFiles to include shared resolution files directly
under ingestion/ (type-env.ts), tagged with the full gated language set via
fileLanguages (valid-if-any). Entries now carry a language SET. Rename
Mode2Result -> ScanResult; fix the header doc (THREE -> FOUR modes).
Gate behavior unchanged until U3 consumes receiverNodeType. Issue #1920.
* feat(ingestion): node-scoped field gate + remove gate-flagged dead literals (U3, U4)
U3: the gate validates childForFieldName lookups node-scoped (validateField with
the captured receiverNodeType) and fails loudly on a degraded/vacuous run
(asserts resolutionLayerProgramOk, floors collected counts, requires
knownFailures empty).
U4: remove every dead field/literal the hardened gate flags — all behavior-neutral
(the dead disjunct never fired on reachable nodes; verified by real parse + the
type-extractor/resolution unit suites, 484 passing):
- type-env.ts: parameterized_type (emitted by no grammar) and switch_block_label
(real Java enhanced switch is switch_label/switch_rule) from the SyntaxNode .type sets
- languages/csharp/captures.ts: generic_name has no `name` field -> firstNamedChild
- type-extractors/jvm.ts: Kotlin property_declaration has no name/type fields
(positional children) -> findChild; drop the else-branch `pattern` fallbacks x2
- type-extractors/csharp.ts: drop the else-branch `pattern` fallback (parity with go/php/python/swift)
Gate green with node-scoped validation on; tsc clean. Closes the Mode-4
type-env coverage opened in U2. Latent follow-up: Java enhanced-switch arms
(switch_rule) are absent from NARROWING_BRANCH_TYPES — a separate behavior fix.
Issue #1920 (epic #1919).
* fix(java): exclude interleaved comments from call arity (U5)
tree-sitter-java emits block_comment/line_comment as named children of
argument_list; counting them inflated @reference.arity / @reference.parameter-
types / @reference.arg-names for any Java call with an inline comment, which
skews arity-based overload resolution (arity feeds call-processor symbol-ID
generation). Filter them at the single arg-list site (also corrects the
downstream args.map). The previously-removed `comment` literal never matched —
the real nodes are block_comment/line_comment (the #1920 gate lesson).
Isolated from the behavior-neutral gate units (U1-U4) since this changes
production graph output. Java resolver suite 178/178; new java-call-arity test
covers block/line comments, leading comment, constructor calls, and the
no-comment regression. Issue #1920 (epic #1919).
* test(ingestion): cover Kotlin/C# multi-arg generics + tighten probe assertions (U6)
- extract-generic-type-args: add real-parse Kotlin Map<String,User>
(user_type > type_arguments > type_projection) and C# Dictionary<string,User>
(generic_name > type_argument_list) multi-arg cases.
- grammar-introspection: the probeNodeType test now asserts 'dead' for a bogus
node on installed grammars (not merely not-throw), and documents the null-model
split (validateField -> unavailable; validateNodeType -> still probes the live
grammar). Issue #1920 (epic #1919).
* style(test): apply root prettier formatting (CI format gate)
CI runs `prettier --check .` from the repo root (printWidth 100); the gitnexus/
pre-commit hook formatted these two files differently. Format-only, no behavior
change. Issue #1920.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
9.5 KiB
TypeScript
206 lines
9.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import Parser from 'tree-sitter';
|
||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||
import {
|
||
getLanguageGrammar,
|
||
isLanguageAvailable,
|
||
} from '../../src/core/tree-sitter/parser-loader.js';
|
||
import {
|
||
GATED_LANGUAGES,
|
||
loadGrammarModel,
|
||
probeNodeType,
|
||
probeField,
|
||
validateNodeType,
|
||
validateField,
|
||
isNodeTypeError,
|
||
isFieldError,
|
||
} from '../helpers/grammar-introspection.js';
|
||
|
||
describe('grammar-introspection helper', () => {
|
||
describe('loadGrammarModel — membership set', () => {
|
||
it('builds named, anonymous, supertype node types and per-node fields for Python', () => {
|
||
const model = loadGrammarModel(SupportedLanguages.Python);
|
||
expect(model).not.toBeNull();
|
||
// named node, anonymous token, and a supertype name are all members
|
||
expect(model!.nodeTypes.has('function_definition')).toBe(true);
|
||
expect(model!.nodeTypes.has('{')).toBe(true);
|
||
expect(model!.nodeTypes.has('expression')).toBe(true);
|
||
// per-node fields
|
||
const fields = model!.fieldsByNode.get('function_definition');
|
||
expect(fields).toBeDefined();
|
||
expect(fields!.has('name')).toBe(true);
|
||
expect(fields!.has('body')).toBe(true);
|
||
expect(fields!.has('parameters')).toBe(true);
|
||
expect(model!.allFields.has('name')).toBe(true);
|
||
});
|
||
|
||
it('unions typescript ∪ tsx so JSX-only nodes are members', () => {
|
||
const model = loadGrammarModel(SupportedLanguages.TypeScript);
|
||
expect(model).not.toBeNull();
|
||
expect(model!.nodeTypes.has('jsx_element')).toBe(true); // tsx-only
|
||
expect(model!.nodeTypes.has('type_annotation')).toBe(true); // typescript
|
||
});
|
||
|
||
it('resolves PHP to the php_only variant (excludes embedded-HTML nodes)', () => {
|
||
const model = loadGrammarModel(SupportedLanguages.PHP);
|
||
expect(model).not.toBeNull();
|
||
expect(model!.nodeTypes.has('function_definition')).toBe(true);
|
||
// text_interpolation exists only in the full `php` (embedded-HTML) grammar
|
||
expect(model!.nodeTypes.has('text_interpolation')).toBe(false);
|
||
});
|
||
|
||
it('excludes COBOL and never throws for any gated language', () => {
|
||
expect(GATED_LANGUAGES).not.toContain(SupportedLanguages.Cobol);
|
||
for (const lang of GATED_LANGUAGES) {
|
||
// returns a model (installed) or null (optional grammar absent) — never throws
|
||
expect(() => loadGrammarModel(lang)).not.toThrow();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('probeNodeType — live-grammar fallback', () => {
|
||
it('classifies an absent node type as dead and a real one as valid (Rust)', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.Rust)) return;
|
||
expect(probeNodeType(SupportedLanguages.Rust, 'method_call_expression')).toBe('dead');
|
||
expect(probeNodeType(SupportedLanguages.Rust, 'call_expression')).toBe('valid');
|
||
});
|
||
|
||
it('accepts an anonymous token via the "x" form (Python)', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.Python)) return;
|
||
expect(probeNodeType(SupportedLanguages.Python, '{')).toBe('valid');
|
||
});
|
||
|
||
it('accepts a supertype via membership without needing a probe (Python)', () => {
|
||
const model = loadGrammarModel(SupportedLanguages.Python);
|
||
expect(validateNodeType(SupportedLanguages.Python, model, 'expression')).toBe('valid');
|
||
});
|
||
|
||
it('classifies a bogus node type as dead for installed grammars (never just not-throw)', () => {
|
||
for (const lang of GATED_LANGUAGES) {
|
||
const verdict = probeNodeType(lang, 'definitely_not_a_node_type_xyz');
|
||
// installed → an absent node type is 'dead'; uninstalled optional grammar → 'unavailable'.
|
||
if (isLanguageAvailable(lang)) {
|
||
expect(verdict, `${lang} should classify a bogus node type as dead`).toBe('dead');
|
||
} else {
|
||
expect(verdict).toBe('unavailable');
|
||
}
|
||
}
|
||
});
|
||
|
||
it('distinguishes the null-model paths: validateField unavailable vs validateNodeType still probes', () => {
|
||
// validateField short-circuits to unavailable with no model (no grammar set).
|
||
expect(validateField(null, 'anything', 'some_node')).toBe('unavailable');
|
||
// validateNodeType, by contrast, still probes the LIVE grammar when the model
|
||
// is null, so for an installed language a bogus node type is 'dead'.
|
||
if (isLanguageAvailable(SupportedLanguages.Python)) {
|
||
expect(validateNodeType(SupportedLanguages.Python, null, 'definitely_not_xyz')).toBe(
|
||
'dead',
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('isNodeTypeError — classifier self-test', () => {
|
||
it('matches the TSQueryErrorNodeType message and rejects valid queries', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.Rust)) return;
|
||
const grammar = getLanguageGrammar(SupportedLanguages.Rust) as ConstructorParameters<
|
||
typeof Parser.Query
|
||
>[0];
|
||
let caught: unknown;
|
||
try {
|
||
// method_call_expression does not exist in tree-sitter-rust
|
||
new Parser.Query(grammar, '(method_call_expression) @_');
|
||
} catch (e) {
|
||
caught = e;
|
||
}
|
||
expect(caught).toBeDefined();
|
||
// If a future tree-sitter bump changes the wording, this fails loudly
|
||
// instead of silently passing every literal.
|
||
expect(isNodeTypeError(caught)).toBe(true);
|
||
// a valid node type compiles without throwing
|
||
expect(() => new Parser.Query(grammar, '(call_expression) @_')).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('validateField', () => {
|
||
it('passes a real node-scoped field and fails a non-existent one', () => {
|
||
const model = loadGrammarModel(SupportedLanguages.Python);
|
||
expect(validateField(model, 'name', 'function_definition')).toBe('valid');
|
||
expect(validateField(model, 'nonexistent_field_xyz', 'function_definition')).toBe('dead');
|
||
});
|
||
|
||
it('rescues a JSON-under-reported / supertype-permissive field via the probe (not a false positive)', () => {
|
||
// C# `parameter` has no `pattern` field (TSQueryErrorStructure), but
|
||
// `binary_expression` accepts `pattern` through its supertype-typed slots,
|
||
// so the probe compiles and validateField must NOT flag it dead. This pins
|
||
// the conservative-toward-valid direction: a membership miss falls through
|
||
// to the probe, never straight to dead.
|
||
if (!isLanguageAvailable(SupportedLanguages.CSharp)) return;
|
||
const model = loadGrammarModel(SupportedLanguages.CSharp);
|
||
expect(validateField(model, 'pattern', 'parameter')).toBe('dead'); // structurally impossible
|
||
expect(validateField(model, 'pattern', 'binary_expression')).toBe('valid'); // probe-rescued
|
||
});
|
||
});
|
||
|
||
describe('probeField — conservative node-scoped field oracle', () => {
|
||
it('classifies a structurally-impossible field as dead (C# parameter/pattern)', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.CSharp)) return;
|
||
// (parameter pattern: (_)) throws TSQueryErrorStructure
|
||
expect(probeField(SupportedLanguages.CSharp, 'parameter', 'pattern')).toBe('dead');
|
||
// an unknown field name throws TSQueryErrorField
|
||
expect(probeField(SupportedLanguages.CSharp, 'parameter', 'total_garbage_field')).toBe(
|
||
'dead',
|
||
);
|
||
// a real field compiles
|
||
expect(probeField(SupportedLanguages.CSharp, 'parameter', 'type')).toBe('valid');
|
||
});
|
||
|
||
it('returns unavailable (not dead) when the node type is absent in the grammar', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.Java)) return;
|
||
// `parameter` is not a Java node (Java uses `formal_parameter`) → NodeType error
|
||
// → unavailable, so multi-language ANY-semantics can defer to the right grammar.
|
||
expect(probeField(SupportedLanguages.Java, 'parameter', 'name')).toBe('unavailable');
|
||
});
|
||
|
||
it('is conservative-toward-valid for supertype-typed fields (never false-positive)', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.CSharp)) return;
|
||
// `binary_expression` has no `pattern` field, but its supertype-typed slots
|
||
// make the query compile → valid. The probe errs toward valid by design.
|
||
expect(probeField(SupportedLanguages.CSharp, 'binary_expression', 'pattern')).toBe('valid');
|
||
});
|
||
|
||
it('never throws for any gated language', () => {
|
||
for (const lang of GATED_LANGUAGES) {
|
||
expect(() => probeField(lang, 'some_node', 'some_field')).not.toThrow();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('isFieldError — classifier self-test', () => {
|
||
it('matches TSQueryErrorStructure and TSQueryErrorField but not NodeType', () => {
|
||
if (!isLanguageAvailable(SupportedLanguages.CSharp)) return;
|
||
const grammar = getLanguageGrammar(SupportedLanguages.CSharp) as ConstructorParameters<
|
||
typeof Parser.Query
|
||
>[0];
|
||
const grab = (q: string): unknown => {
|
||
try {
|
||
new Parser.Query(grammar, q);
|
||
return undefined;
|
||
} catch (e) {
|
||
return e;
|
||
}
|
||
};
|
||
const structureErr = grab('(parameter pattern: (_)) @_'); // TSQueryErrorStructure
|
||
const fieldErr = grab('(parameter total_garbage_field: (_)) @_'); // TSQueryErrorField
|
||
const nodeTypeErr = grab('(nonexistent_node_xyz) @_'); // TSQueryErrorNodeType
|
||
expect(structureErr).toBeDefined();
|
||
expect(fieldErr).toBeDefined();
|
||
expect(nodeTypeErr).toBeDefined();
|
||
expect(isFieldError(structureErr)).toBe(true);
|
||
expect(isFieldError(fieldErr)).toBe(true);
|
||
// a node-type error is NOT a field error (it routes to `unavailable`, not `dead`)
|
||
expect(isFieldError(nodeTypeErr)).toBe(false);
|
||
expect(isNodeTypeError(nodeTypeErr)).toBe(true);
|
||
});
|
||
});
|
||
});
|