mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +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>
This commit is contained in:
parent
d1d2a64d0f
commit
af9d709fdd
32 changed files with 1334 additions and 209 deletions
|
|
@ -73,11 +73,6 @@ const CSHARP_DECL_TYPES = new Set([
|
||||||
'struct_declaration',
|
'struct_declaration',
|
||||||
'enum_declaration',
|
'enum_declaration',
|
||||||
'record_declaration',
|
'record_declaration',
|
||||||
// tree-sitter-c-sharp absorbs 'record struct' and 'record class' into
|
|
||||||
// record_declaration — these two node types are listed defensively but
|
|
||||||
// never emitted by the grammar in practice (verified against ^0.23.1).
|
|
||||||
'record_struct_declaration',
|
|
||||||
'record_class_declaration',
|
|
||||||
'delegate_declaration',
|
'delegate_declaration',
|
||||||
'property_declaration',
|
'property_declaration',
|
||||||
'field_declaration',
|
'field_declaration',
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,7 @@ export const dartConfig: FieldExtractionConfig = {
|
||||||
// declaration > type_identifier (first named child usually)
|
// declaration > type_identifier (first named child usually)
|
||||||
for (let i = 0; i < node.namedChildCount; i++) {
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
const child = node.namedChild(i);
|
const child = node.namedChild(i);
|
||||||
if (
|
if (child && (child.type === 'type_identifier' || child.type === 'function_type')) {
|
||||||
child &&
|
|
||||||
(child.type === 'type_identifier' ||
|
|
||||||
child.type === 'generic_type' ||
|
|
||||||
child.type === 'function_type')
|
|
||||||
) {
|
|
||||||
return extractSimpleTypeName(child) ?? child.text?.trim();
|
return extractSimpleTypeName(child) ?? child.text?.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,7 @@ export const phpConfig: FieldExtractionConfig = {
|
||||||
child.type === 'named_type' ||
|
child.type === 'named_type' ||
|
||||||
child.type === 'optional_type' ||
|
child.type === 'optional_type' ||
|
||||||
child.type === 'primitive_type' ||
|
child.type === 'primitive_type' ||
|
||||||
child.type === 'intersection_type' ||
|
child.type === 'intersection_type'
|
||||||
child.type === 'nullable_type'
|
|
||||||
) {
|
) {
|
||||||
return extractSimpleTypeName(child) ?? child.text?.trim();
|
return extractSimpleTypeName(child) ?? child.text?.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const SWIFT_VIS = new Set<FieldVisibility>([
|
||||||
*/
|
*/
|
||||||
export const swiftConfig: FieldExtractionConfig = {
|
export const swiftConfig: FieldExtractionConfig = {
|
||||||
language: SupportedLanguages.Swift,
|
language: SupportedLanguages.Swift,
|
||||||
typeDeclarationNodes: ['class_declaration', 'struct_declaration', 'protocol_declaration'],
|
typeDeclarationNodes: ['class_declaration', 'protocol_declaration'],
|
||||||
fieldNodeTypes: ['property_declaration'],
|
fieldNodeTypes: ['property_declaration'],
|
||||||
bodyNodeTypes: ['class_body', 'protocol_body'],
|
bodyNodeTypes: ['class_body', 'protocol_body'],
|
||||||
defaultVisibility: 'internal',
|
defaultVisibility: 'internal',
|
||||||
|
|
|
||||||
|
|
@ -86,18 +86,6 @@ export class TypeScriptFieldExtractor extends BaseFieldExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for modifier node (tree-sitter typescript may group these)
|
|
||||||
const modifiers = node.childForFieldName('modifiers');
|
|
||||||
if (modifiers) {
|
|
||||||
for (let i = 0; i < modifiers.childCount; i++) {
|
|
||||||
const modifier = modifiers.child(i);
|
|
||||||
const modText = modifier?.text.trim() as FieldVisibility | undefined;
|
|
||||||
if (modText && TypeScriptFieldExtractor.VISIBILITY_MODIFIERS.has(modText)) {
|
|
||||||
return modText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeScript class members are public by default
|
// TypeScript class members are public by default
|
||||||
return 'public';
|
return 'public';
|
||||||
}
|
}
|
||||||
|
|
@ -113,16 +101,6 @@ export class TypeScriptFieldExtractor extends BaseFieldExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = node.childForFieldName('modifiers');
|
|
||||||
if (modifiers) {
|
|
||||||
for (let i = 0; i < modifiers.childCount; i++) {
|
|
||||||
const modifier = modifiers.child(i);
|
|
||||||
if (modifier && modifier.text === 'static') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,16 +115,6 @@ export class TypeScriptFieldExtractor extends BaseFieldExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = node.childForFieldName('modifiers');
|
|
||||||
if (modifiers) {
|
|
||||||
for (let i = 0; i < modifiers.childCount; i++) {
|
|
||||||
const modifier = modifiers.child(i);
|
|
||||||
if (modifier && modifier.text === 'readonly') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export function computeCppDeclarationArity(node: SyntaxNode): CppArityInfo {
|
||||||
if (
|
if (
|
||||||
child.type === 'parameter_declaration' ||
|
child.type === 'parameter_declaration' ||
|
||||||
child.type === 'optional_parameter_declaration' ||
|
child.type === 'optional_parameter_declaration' ||
|
||||||
child.type === 'variadic_parameter' ||
|
|
||||||
child.type === 'variadic_parameter_declaration'
|
child.type === 'variadic_parameter_declaration'
|
||||||
) {
|
) {
|
||||||
params.push(child);
|
params.push(child);
|
||||||
|
|
@ -60,11 +59,7 @@ export function computeCppDeclarationArity(node: SyntaxNode): CppArityInfo {
|
||||||
// token in tree-sitter-cpp, detected via `hasEllipsis` above.
|
// token in tree-sitter-cpp, detected via `hasEllipsis` above.
|
||||||
// C++ parameter packs: `template<typename... Ts> void foo(Ts... args)` —
|
// C++ parameter packs: `template<typename... Ts> void foo(Ts... args)` —
|
||||||
// detected as `variadic_parameter_declaration`.
|
// detected as `variadic_parameter_declaration`.
|
||||||
const isVariadic =
|
const isVariadic = hasEllipsis || params.some((p) => p.type === 'variadic_parameter_declaration');
|
||||||
hasEllipsis ||
|
|
||||||
params.some(
|
|
||||||
(p) => p.type === 'variadic_parameter' || p.type === 'variadic_parameter_declaration',
|
|
||||||
);
|
|
||||||
const optionalCount = params.filter((p) => p.type === 'optional_parameter_declaration').length;
|
const optionalCount = params.filter((p) => p.type === 'optional_parameter_declaration').length;
|
||||||
const requiredCount = params.filter(
|
const requiredCount = params.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
|
|
@ -77,10 +72,7 @@ export function computeCppDeclarationArity(node: SyntaxNode): CppArityInfo {
|
||||||
const types: string[] = [];
|
const types: string[] = [];
|
||||||
const typeClasses: ParameterTypeClass[] = [];
|
const typeClasses: ParameterTypeClass[] = [];
|
||||||
for (const p of params) {
|
for (const p of params) {
|
||||||
if (p.type === 'variadic_parameter') {
|
if (p.type === 'variadic_parameter_declaration') {
|
||||||
types.push('...');
|
|
||||||
typeClasses.push(unknownTypeClass('...'));
|
|
||||||
} else if (p.type === 'variadic_parameter_declaration') {
|
|
||||||
// Parameter pack: treated as variadic
|
// Parameter pack: treated as variadic
|
||||||
types.push('...');
|
types.push('...');
|
||||||
typeClasses.push(unknownTypeClass('...'));
|
typeClasses.push(unknownTypeClass('...'));
|
||||||
|
|
|
||||||
|
|
@ -1360,7 +1360,7 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo | null {
|
||||||
inner = next;
|
inner = next;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (inner.type === 'reference_declarator' || inner.type === 'rvalue_reference_declarator') {
|
if (inner.type === 'reference_declarator') {
|
||||||
// reference_declarator has a single child (the inner declarator).
|
// reference_declarator has a single child (the inner declarator).
|
||||||
let next: SyntaxNode | null = null;
|
let next: SyntaxNode | null = null;
|
||||||
for (let j = 0; j < inner.namedChildCount; j++) {
|
for (let j = 0; j < inner.namedChildCount; j++) {
|
||||||
|
|
|
||||||
|
|
@ -165,10 +165,7 @@ export function emitJavaScopeCaptures(
|
||||||
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
|
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
|
||||||
if (callNode !== null) {
|
if (callNode !== null) {
|
||||||
const argList = callNode.childForFieldName('arguments');
|
const argList = callNode.childForFieldName('arguments');
|
||||||
const args =
|
const args = argList === null ? [] : argList.namedChildren.filter((c) => c !== null);
|
||||||
argList === null
|
|
||||||
? []
|
|
||||||
: argList.namedChildren.filter((c) => c !== null && c.type !== 'comment');
|
|
||||||
grouped['@reference.arity'] = syntheticCapture(
|
grouped['@reference.arity'] = syntheticCapture(
|
||||||
'@reference.arity',
|
'@reference.arity',
|
||||||
callNode,
|
callNode,
|
||||||
|
|
|
||||||
|
|
@ -116,23 +116,7 @@ function parseUseClause(clause: SyntaxNode, qualifier: PhpImportKind): PhpImport
|
||||||
const source = qualName.text.trim();
|
const source = qualName.text.trim();
|
||||||
if (source === '') return null;
|
if (source === '') return null;
|
||||||
|
|
||||||
// Strategy 1: explicit alias_clause wrapper (older grammar versions).
|
// Strategy: bare sibling `name` node after the qualified_name.
|
||||||
const aliasClause = findNamedChild(clause, 'alias_clause');
|
|
||||||
if (aliasClause !== null) {
|
|
||||||
// alias_clause: "as" name
|
|
||||||
const aliasName = findNamedChild(aliasClause, 'name') ?? aliasClause.firstNamedChild;
|
|
||||||
const alias = aliasName?.text.trim() ?? '';
|
|
||||||
if (alias === '') return null;
|
|
||||||
return {
|
|
||||||
kind: 'alias',
|
|
||||||
source,
|
|
||||||
name: alias,
|
|
||||||
alias,
|
|
||||||
atNode: clause,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: bare sibling `name` node after the qualified_name.
|
|
||||||
// tree-sitter-php (≥ 0.22) emits `use Foo\Bar as Baz` as:
|
// tree-sitter-php (≥ 0.22) emits `use Foo\Bar as Baz` as:
|
||||||
// namespace_use_clause
|
// namespace_use_clause
|
||||||
// qualified_name "Foo\Bar"
|
// qualified_name "Foo\Bar"
|
||||||
|
|
@ -231,22 +215,7 @@ function parseInnerClause(
|
||||||
|
|
||||||
const source = prefix !== '' ? `${prefix}\\${innerPath}` : innerPath;
|
const source = prefix !== '' ? `${prefix}\\${innerPath}` : innerPath;
|
||||||
|
|
||||||
// Strategy 1: explicit alias_clause wrapper (older grammar versions).
|
// Strategy: bare sibling `name` node after the qualified_name (tree-sitter-php ≥ 0.22).
|
||||||
const aliasClause = findNamedChild(clause, 'alias_clause');
|
|
||||||
if (aliasClause !== null) {
|
|
||||||
const aliasName = findNamedChild(aliasClause, 'name') ?? aliasClause.firstNamedChild;
|
|
||||||
const alias = aliasName?.text.trim() ?? '';
|
|
||||||
if (alias === '') return null;
|
|
||||||
return {
|
|
||||||
kind: 'alias',
|
|
||||||
source,
|
|
||||||
name: alias,
|
|
||||||
alias,
|
|
||||||
atNode: clause,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: bare sibling `name` node after the qualified_name (tree-sitter-php ≥ 0.22).
|
|
||||||
if (clause.namedChildCount >= 2) {
|
if (clause.namedChildCount >= 2) {
|
||||||
const lastChild = clause.namedChild(clause.namedChildCount - 1);
|
const lastChild = clause.namedChild(clause.namedChildCount - 1);
|
||||||
if (lastChild !== null && lastChild !== qualName && lastChild.type === 'name') {
|
if (lastChild !== null && lastChild !== qualName && lastChild.type === 'name') {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ const TYPE_DECL_NODE_TYPES = new Set([
|
||||||
'interface_declaration',
|
'interface_declaration',
|
||||||
'trait_declaration',
|
'trait_declaration',
|
||||||
'enum_declaration',
|
'enum_declaration',
|
||||||
|
// tree-sitter-php node for `new class {...}` (real node is `anonymous_class`,
|
||||||
|
// not `anonymous_class_declaration`). Included so the enclosing-type walk
|
||||||
|
// stops AT the anon class and the guard below skips it (otherwise a method in
|
||||||
|
// an anon class nested in a named class would mis-bind $this to the outer class).
|
||||||
|
'anonymous_class',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const FUNCTION_NODE_TYPES = new Set([
|
const FUNCTION_NODE_TYPES = new Set([
|
||||||
|
|
@ -98,7 +103,7 @@ export function synthesizePhpReceiverBinding(fnNode: SyntaxNode): CaptureMatch[]
|
||||||
if (enclosingType === null) return [];
|
if (enclosingType === null) return [];
|
||||||
|
|
||||||
// Anonymous class — skip (no stable name).
|
// Anonymous class — skip (no stable name).
|
||||||
if (enclosingType.type === 'anonymous_class_declaration') return [];
|
if (enclosingType.type === 'anonymous_class') return [];
|
||||||
|
|
||||||
const enclosingName = typeName(enclosingType);
|
const enclosingName = typeName(enclosingType);
|
||||||
if (enclosingName === null) return [];
|
if (enclosingName === null) return [];
|
||||||
|
|
@ -106,10 +111,8 @@ export function synthesizePhpReceiverBinding(fnNode: SyntaxNode): CaptureMatch[]
|
||||||
// Anchor the synthesized captures to the method body (compound_statement)
|
// Anchor the synthesized captures to the method body (compound_statement)
|
||||||
// so they land inside the function scope, not at the class scope.
|
// so they land inside the function scope, not at the class scope.
|
||||||
// For interface/abstract methods that have no body, skip.
|
// For interface/abstract methods that have no body, skip.
|
||||||
const bodyNode =
|
// tree-sitter-php arrow_function also exposes its expression via the `body` field.
|
||||||
fnNode.childForFieldName('body') ??
|
const bodyNode = fnNode.childForFieldName('body');
|
||||||
// arrow_function: body is the expression after `=>`
|
|
||||||
fnNode.childForFieldName('return_value');
|
|
||||||
if (bodyNode === null) return [];
|
if (bodyNode === null) return [];
|
||||||
|
|
||||||
const out: CaptureMatch[] = [];
|
const out: CaptureMatch[] = [];
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function synthesizeDependsReferences(fnNode: SyntaxNode): readonly Captur
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValue = param.childForFieldName('value') ?? param.childForFieldName('default');
|
const defaultValue = param.childForFieldName('value');
|
||||||
if (defaultValue === null) continue;
|
if (defaultValue === null) continue;
|
||||||
|
|
||||||
const callNode = defaultValue.type === 'call' ? defaultValue : null;
|
const callNode = defaultValue.type === 'call' ? defaultValue : null;
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ export function emitRubyScopeCaptures(
|
||||||
if (argList !== null) {
|
if (argList !== null) {
|
||||||
for (let ai = 0; ai < argList.namedChildCount; ai++) {
|
for (let ai = 0; ai < argList.namedChildCount; ai++) {
|
||||||
const arg = argList.namedChild(ai);
|
const arg = argList.namedChild(ai);
|
||||||
if (arg !== null && (arg.type === 'simple_symbol' || arg.type === 'symbol')) {
|
if (arg !== null && arg.type === 'simple_symbol') {
|
||||||
const propName = arg.text.replace(/^:/, '');
|
const propName = arg.text.replace(/^:/, '');
|
||||||
out.push({
|
out.push({
|
||||||
'@import.statement': grouped['@reference.call.free']!,
|
'@import.statement': grouped['@reference.call.free']!,
|
||||||
|
|
@ -327,7 +327,7 @@ export function emitRubyScopeCaptures(
|
||||||
if (argList !== null) {
|
if (argList !== null) {
|
||||||
for (let ai = 0; ai < argList.namedChildCount; ai++) {
|
for (let ai = 0; ai < argList.namedChildCount; ai++) {
|
||||||
const arg = argList.namedChild(ai);
|
const arg = argList.namedChild(ai);
|
||||||
if (arg !== null && (arg.type === 'simple_symbol' || arg.type === 'symbol')) {
|
if (arg !== null && arg.type === 'simple_symbol') {
|
||||||
const propName = arg.text.replace(/^:/, '');
|
const propName = arg.text.replace(/^:/, '');
|
||||||
out.push({
|
out.push({
|
||||||
'@type-binding.return': syntheticCapture('@type-binding.return', attrNode, text),
|
'@type-binding.return': syntheticCapture('@type-binding.return', attrNode, text),
|
||||||
|
|
|
||||||
|
|
@ -310,9 +310,9 @@ function processStructDestructuring(
|
||||||
for (const fieldNode of patternNode.namedChildren) {
|
for (const fieldNode of patternNode.namedChildren) {
|
||||||
let fieldName: string | undefined;
|
let fieldName: string | undefined;
|
||||||
if (fieldNode.type === 'field_pattern') {
|
if (fieldNode.type === 'field_pattern') {
|
||||||
|
// shorthand `{ a }` and full `{ b: c }` are both field_pattern; the
|
||||||
|
// `name` field is shorthand_field_identifier or field_identifier.
|
||||||
fieldName = fieldNode.childForFieldName('name')?.text;
|
fieldName = fieldNode.childForFieldName('name')?.text;
|
||||||
} else if (fieldNode.type === 'shorthand_field_pattern') {
|
|
||||||
fieldName = fieldNode.firstNamedChild?.text;
|
|
||||||
}
|
}
|
||||||
if (fieldName === undefined) continue;
|
if (fieldName === undefined) continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ const TYPE_DECL_NODE_TYPES = new Set([
|
||||||
'class_declaration',
|
'class_declaration',
|
||||||
'abstract_class_declaration',
|
'abstract_class_declaration',
|
||||||
'class',
|
'class',
|
||||||
'class_expression',
|
|
||||||
'interface_declaration',
|
'interface_declaration',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||||
/** Type node types that represent a return type in function/getter/setter signatures. */
|
/** Type node types that represent a return type in function/getter/setter signatures. */
|
||||||
const TYPE_NODE_TYPES = new Set([
|
const TYPE_NODE_TYPES = new Set([
|
||||||
'type_identifier',
|
'type_identifier',
|
||||||
'generic_type',
|
|
||||||
'function_type',
|
'function_type',
|
||||||
'nullable_type',
|
'nullable_type',
|
||||||
'void_type',
|
'void_type',
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ function extractPhpReturnType(node: SyntaxNode): string | undefined {
|
||||||
'named_type',
|
'named_type',
|
||||||
'union_type',
|
'union_type',
|
||||||
'optional_type',
|
'optional_type',
|
||||||
'nullable_type',
|
|
||||||
'intersection_type',
|
'intersection_type',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,14 +142,14 @@ const extractInitializer: InitializerExtractor = (
|
||||||
const templateFunc =
|
const templateFunc =
|
||||||
func.type === 'template_function'
|
func.type === 'template_function'
|
||||||
? func
|
? func
|
||||||
: func.type === 'qualified_identifier' || func.type === 'scoped_identifier'
|
: func.type === 'qualified_identifier'
|
||||||
? (func.namedChildren.find((c: SyntaxNode) => c.type === 'template_function') ?? null)
|
? (func.namedChildren.find((c: SyntaxNode) => c.type === 'template_function') ?? null)
|
||||||
: null;
|
: null;
|
||||||
if (templateFunc) {
|
if (templateFunc) {
|
||||||
const nameNode = templateFunc.firstNamedChild;
|
const nameNode = templateFunc.firstNamedChild;
|
||||||
if (nameNode) {
|
if (nameNode) {
|
||||||
const funcName =
|
const funcName =
|
||||||
nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier'
|
nameNode.type === 'qualified_identifier'
|
||||||
? (nameNode.lastNamedChild?.text ?? '')
|
? (nameNode.lastNamedChild?.text ?? '')
|
||||||
: nameNode.text;
|
: nameNode.text;
|
||||||
if (SMART_PTR_FACTORIES.has(funcName)) {
|
if (SMART_PTR_FACTORIES.has(funcName)) {
|
||||||
|
|
@ -214,7 +214,7 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||||
if (!value || value.type !== 'call_expression') return undefined;
|
if (!value || value.type !== 'call_expression') return undefined;
|
||||||
const func = value.childForFieldName('function');
|
const func = value.childForFieldName('function');
|
||||||
if (!func) return undefined;
|
if (!func) return undefined;
|
||||||
if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') {
|
if (func.type === 'qualified_identifier') {
|
||||||
const last = func.lastNamedChild;
|
const last = func.lastNamedChild;
|
||||||
if (!last) return undefined;
|
if (!last) return undefined;
|
||||||
const nameNode = declarator.childForFieldName('declarator');
|
const nameNode = declarator.childForFieldName('declarator');
|
||||||
|
|
@ -331,17 +331,13 @@ const extractCppElementTypeFromTypeNode = (
|
||||||
const args = extractCppTemplateTypeArgs(typeNode);
|
const args = extractCppTemplateTypeArgs(typeNode);
|
||||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||||
}
|
}
|
||||||
// reference/pointer types: unwrap and recurse (vector<User>& → vector<User>)
|
// type_descriptor wrapper: unwrap and recurse (vector<User>& → vector<User>)
|
||||||
if (
|
if (typeNode.type === 'type_descriptor') {
|
||||||
typeNode.type === 'reference_type' ||
|
|
||||||
typeNode.type === 'pointer_type' ||
|
|
||||||
typeNode.type === 'type_descriptor'
|
|
||||||
) {
|
|
||||||
const inner = typeNode.lastNamedChild;
|
const inner = typeNode.lastNamedChild;
|
||||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||||
}
|
}
|
||||||
// qualified/scoped types: std::vector<User> → unwrap to template_type child
|
// qualified types: std::vector<User> → unwrap to template_type child
|
||||||
if (typeNode.type === 'qualified_identifier' || typeNode.type === 'scoped_type_identifier') {
|
if (typeNode.type === 'qualified_identifier') {
|
||||||
const inner = typeNode.lastNamedChild;
|
const inner = typeNode.lastNamedChild;
|
||||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
if (inner) return extractCppElementTypeFromTypeNode(inner, pos, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
@ -527,7 +523,7 @@ const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) =>
|
||||||
const nameNode = func.firstNamedChild;
|
const nameNode = func.firstNamedChild;
|
||||||
if (!nameNode) return undefined;
|
if (!nameNode) return undefined;
|
||||||
let funcName: string;
|
let funcName: string;
|
||||||
if (nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier') {
|
if (nameNode.type === 'qualified_identifier') {
|
||||||
funcName = nameNode.lastNamedChild?.text ?? '';
|
funcName = nameNode.lastNamedChild?.text ?? '';
|
||||||
} else {
|
} else {
|
||||||
funcName = nameNode.text;
|
funcName = nameNode.text;
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ const extractDeclaration: TypeBindingExtractor = (
|
||||||
const child = node.namedChild(i);
|
const child = node.namedChild(i);
|
||||||
if (!child) continue;
|
if (!child) continue;
|
||||||
|
|
||||||
if (!typeNode && child.type !== 'variable_declarator' && child.type !== 'equals_value_clause') {
|
if (!typeNode && child.type !== 'variable_declarator') {
|
||||||
// First non-declarator child is the type (identifier, implicit_type, generic_name, etc.)
|
// First non-declarator child is the type (identifier, implicit_type, generic_name, etc.)
|
||||||
typeNode = child;
|
typeNode = child;
|
||||||
}
|
}
|
||||||
|
|
@ -67,12 +67,9 @@ const extractDeclaration: TypeBindingExtractor = (
|
||||||
let typeName: string | undefined;
|
let typeName: string | undefined;
|
||||||
if (typeNode.type === 'implicit_type' && typeNode.text === 'var') {
|
if (typeNode.type === 'implicit_type' && typeNode.text === 'var') {
|
||||||
// Try to infer from initializer: var x = new Foo()
|
// Try to infer from initializer: var x = new Foo()
|
||||||
// tree-sitter-c-sharp may put object_creation_expression as direct child
|
// tree-sitter-c-sharp puts object_creation_expression as a direct child
|
||||||
// or inside equals_value_clause depending on grammar version
|
|
||||||
if (declarators.length === 1) {
|
if (declarators.length === 1) {
|
||||||
const initializer =
|
const initializer = findChild(declarators[0], 'object_creation_expression');
|
||||||
findChild(declarators[0], 'object_creation_expression') ??
|
|
||||||
findChild(declarators[0], 'equals_value_clause')?.firstNamedChild;
|
|
||||||
if (initializer?.type === 'object_creation_expression') {
|
if (initializer?.type === 'object_creation_expression') {
|
||||||
const ctorType = initializer.childForFieldName('type');
|
const ctorType = initializer.childForFieldName('type');
|
||||||
if (ctorType) typeName = extractSimpleTypeName(ctorType);
|
if (ctorType) typeName = extractSimpleTypeName(ctorType);
|
||||||
|
|
@ -131,16 +128,12 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||||
if (!declarator) return undefined;
|
if (!declarator) return undefined;
|
||||||
const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild;
|
const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild;
|
||||||
if (!nameNode || nameNode.type !== 'identifier') return undefined;
|
if (!nameNode || nameNode.type !== 'identifier') return undefined;
|
||||||
// Find the initializer value: either inside equals_value_clause or as a direct child
|
// Find the initializer value as a direct child
|
||||||
// (tree-sitter-c-sharp puts invocation_expression directly inside variable_declarator)
|
// (tree-sitter-c-sharp puts invocation_expression directly inside variable_declarator)
|
||||||
let value: SyntaxNode | null = null;
|
let value: SyntaxNode | null = null;
|
||||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||||
const child = declarator.namedChild(i);
|
const child = declarator.namedChild(i);
|
||||||
if (!child) continue;
|
if (!child) continue;
|
||||||
if (child.type === 'equals_value_clause') {
|
|
||||||
value = child.firstNamedChild;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
child.type === 'invocation_expression' ||
|
child.type === 'invocation_expression' ||
|
||||||
child.type === 'object_creation_expression' ||
|
child.type === 'object_creation_expression' ||
|
||||||
|
|
@ -471,20 +464,9 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||||
if (!nameNode) continue;
|
if (!nameNode) continue;
|
||||||
const lhs = nameNode.text;
|
const lhs = nameNode.text;
|
||||||
if (scopeEnv.has(lhs)) continue;
|
if (scopeEnv.has(lhs)) continue;
|
||||||
// C# wraps value in equals_value_clause; fall back to last named child
|
// C# variable_declarator holds the initializer value as a direct named child
|
||||||
let evc: SyntaxNode | null = null;
|
const valueNode = child.namedChild(child.namedChildCount - 1);
|
||||||
for (let j = 0; j < child.childCount; j++) {
|
if (valueNode && valueNode !== nameNode && valueNode.type === 'identifier') {
|
||||||
if (child.child(j)?.type === 'equals_value_clause') {
|
|
||||||
evc = child.child(j);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const valueNode = evc?.firstNamedChild ?? child.namedChild(child.namedChildCount - 1);
|
|
||||||
if (
|
|
||||||
valueNode &&
|
|
||||||
valueNode !== nameNode &&
|
|
||||||
(valueNode.type === 'identifier' || valueNode.type === 'simple_identifier')
|
|
||||||
) {
|
|
||||||
return { kind: 'copy', lhs, rhs: valueNode.text };
|
return { kind: 'copy', lhs, rhs: valueNode.text };
|
||||||
}
|
}
|
||||||
// member_access_expression RHS → fieldAccess (a.Field)
|
// member_access_expression RHS → fieldAccess (a.Field)
|
||||||
|
|
@ -498,7 +480,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||||
// invocation_expression RHS
|
// invocation_expression RHS
|
||||||
if (valueNode?.type === 'invocation_expression') {
|
if (valueNode?.type === 'invocation_expression') {
|
||||||
const funcNode = valueNode.firstNamedChild;
|
const funcNode = valueNode.firstNamedChild;
|
||||||
if (funcNode?.type === 'identifier_name' || funcNode?.type === 'identifier') {
|
if (funcNode?.type === 'identifier') {
|
||||||
return { kind: 'callResult', lhs, callee: funcNode.text };
|
return { kind: 'callResult', lhs, callee: funcNode.text };
|
||||||
}
|
}
|
||||||
// method call with receiver → methodCallResult: a.GetC()
|
// method call with receiver → methodCallResult: a.GetC()
|
||||||
|
|
@ -515,7 +497,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||||
const inner = valueNode.firstNamedChild;
|
const inner = valueNode.firstNamedChild;
|
||||||
if (inner?.type === 'invocation_expression') {
|
if (inner?.type === 'invocation_expression') {
|
||||||
const funcNode = inner.firstNamedChild;
|
const funcNode = inner.firstNamedChild;
|
||||||
if (funcNode?.type === 'identifier_name' || funcNode?.type === 'identifier') {
|
if (funcNode?.type === 'identifier') {
|
||||||
return { kind: 'callResult', lhs, callee: funcNode.text };
|
return { kind: 'callResult', lhs, callee: funcNode.text };
|
||||||
}
|
}
|
||||||
if (funcNode?.type === 'member_access_expression') {
|
if (funcNode?.type === 'member_access_expression') {
|
||||||
|
|
@ -565,9 +547,7 @@ export const typeConfig: LanguageTypeConfig = {
|
||||||
const direct = node.childForFieldName('type');
|
const direct = node.childForFieldName('type');
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
|
|
||||||
const wrapped =
|
const wrapped = (() => {
|
||||||
node.childForFieldName('declaration') ??
|
|
||||||
(() => {
|
|
||||||
for (let i = 0; i < node.namedChildCount; i++) {
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
const c = node.namedChild(i);
|
const c = node.namedChild(i);
|
||||||
if (c?.type === 'variable_declaration') return c;
|
if (c?.type === 'variable_declaration') return c;
|
||||||
|
|
|
||||||
|
|
@ -145,16 +145,8 @@ const extractDeclaration: TypeBindingExtractor = (
|
||||||
|
|
||||||
/** Go: parameter → name type */
|
/** Go: parameter → name type */
|
||||||
const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||||
let nameNode: SyntaxNode | null = null;
|
const nameNode = node.childForFieldName('name');
|
||||||
let typeNode: SyntaxNode | null = null;
|
const typeNode = node.childForFieldName('type');
|
||||||
|
|
||||||
if (node.type === 'parameter') {
|
|
||||||
nameNode = node.childForFieldName('name');
|
|
||||||
typeNode = node.childForFieldName('type');
|
|
||||||
} else {
|
|
||||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
|
||||||
typeNode = node.childForFieldName('type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nameNode || !typeNode) return;
|
if (!nameNode || !typeNode) return;
|
||||||
const varName = extractVarName(nameNode);
|
const varName = extractVarName(nameNode);
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
nameNode = node.childForFieldName('name');
|
nameNode = node.childForFieldName('name');
|
||||||
} else {
|
} else {
|
||||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
nameNode = node.childForFieldName('name');
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
||||||
nameNode = node.childForFieldName('name');
|
nameNode = node.childForFieldName('name');
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
} else {
|
} else {
|
||||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
nameNode = node.childForFieldName('name');
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
// Python typed_parameter: name is a positional child (identifier), not a named field
|
// Python typed_parameter: name is a positional child (identifier), not a named field
|
||||||
if (!nameNode && node.type === 'typed_parameter') {
|
if (!nameNode && node.type === 'typed_parameter') {
|
||||||
|
|
|
||||||
|
|
@ -391,8 +391,8 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||||
const rhsNode = node.childForFieldName('right');
|
const rhsNode = node.childForFieldName('right');
|
||||||
if (!rhsNode) return undefined;
|
if (!rhsNode) return undefined;
|
||||||
if (rhsNode.type === 'identifier') return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
|
if (rhsNode.type === 'identifier') return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
|
||||||
// call/method_call RHS — Ruby uses method calls for both field access and method calls
|
// call RHS — Ruby uses method calls for both field access and method calls
|
||||||
if (rhsNode.type === 'call' || rhsNode.type === 'method_call') {
|
if (rhsNode.type === 'call') {
|
||||||
const methodNode = rhsNode.childForFieldName('method');
|
const methodNode = rhsNode.childForFieldName('method');
|
||||||
const receiverNode = rhsNode.childForFieldName('receiver');
|
const receiverNode = rhsNode.childForFieldName('receiver');
|
||||||
if (!receiverNode && methodNode?.type === 'identifier') {
|
if (!receiverNode && methodNode?.type === 'identifier') {
|
||||||
|
|
|
||||||
|
|
@ -277,16 +277,6 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
||||||
return { kind: 'callResult', lhs, callee: funcNode.text };
|
return { kind: 'callResult', lhs, callee: funcNode.text };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// method_call_expression RHS → methodCallResult (receiver.method())
|
|
||||||
if (unwrapped.type === 'method_call_expression') {
|
|
||||||
const obj = unwrapped.firstNamedChild;
|
|
||||||
if (obj?.type === 'identifier') {
|
|
||||||
const methodNode = unwrapped.childForFieldName('name') ?? unwrapped.namedChild(1);
|
|
||||||
if (methodNode?.type === 'field_identifier') {
|
|
||||||
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: methodNode.text };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -410,11 +400,6 @@ const extractRustElementTypeFromTypeNode = (
|
||||||
const elemNode = typeNode.firstNamedChild;
|
const elemNode = typeNode.firstNamedChild;
|
||||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
if (elemNode) return extractSimpleTypeName(elemNode);
|
||||||
}
|
}
|
||||||
// slice_type: [User] — element is the first child
|
|
||||||
if (typeNode.type === 'slice_type') {
|
|
||||||
const elemNode = typeNode.firstNamedChild;
|
|
||||||
if (elemNode) return extractSimpleTypeName(elemNode);
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,11 +257,7 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode, depth = 0): string |
|
||||||
|
|
||||||
// Generic types: extract the base type (e.g., List<User> → List)
|
// Generic types: extract the base type (e.g., List<User> → List)
|
||||||
// For nullable wrappers (Optional<User>, Option<User>), unwrap to inner type.
|
// For nullable wrappers (Optional<User>, Option<User>), unwrap to inner type.
|
||||||
if (
|
if (typeNode.type === 'generic_type' || typeNode.type === 'generic_name') {
|
||||||
typeNode.type === 'generic_type' ||
|
|
||||||
typeNode.type === 'parameterized_type' ||
|
|
||||||
typeNode.type === 'generic_name'
|
|
||||||
) {
|
|
||||||
const base =
|
const base =
|
||||||
typeNode.childForFieldName('name') ??
|
typeNode.childForFieldName('name') ??
|
||||||
typeNode.childForFieldName('type') ??
|
typeNode.childForFieldName('type') ??
|
||||||
|
|
@ -431,11 +427,7 @@ export const extractGenericTypeArgs = (typeNode: SyntaxNode, depth = 0): string[
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process generic/parameterized type nodes (includes C#'s generic_name)
|
// Only process generic/parameterized type nodes (includes C#'s generic_name)
|
||||||
if (
|
if (typeNode.type !== 'generic_type' && typeNode.type !== 'generic_name') {
|
||||||
typeNode.type !== 'generic_type' &&
|
|
||||||
typeNode.type !== 'parameterized_type' &&
|
|
||||||
typeNode.type !== 'generic_name'
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const extractDeclaration: TypeBindingExtractor = (
|
||||||
env: Map<string, string>,
|
env: Map<string, string>,
|
||||||
): void => {
|
): void => {
|
||||||
// Swift property_declaration has pattern and type_annotation
|
// Swift property_declaration has pattern and type_annotation
|
||||||
const pattern = node.childForFieldName('pattern') ?? findChild(node, 'pattern');
|
const pattern = findChild(node, 'pattern');
|
||||||
const typeAnnotation = node.childForFieldName('type') ?? findChild(node, 'type_annotation');
|
const typeAnnotation = node.childForFieldName('type') ?? findChild(node, 'type_annotation');
|
||||||
if (!pattern || !typeAnnotation) return;
|
if (!pattern || !typeAnnotation) return;
|
||||||
const varName = extractVarName(pattern) ?? pattern.text;
|
const varName = extractVarName(pattern) ?? pattern.text;
|
||||||
|
|
@ -65,10 +65,10 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
||||||
let typeNode: SyntaxNode | null = null;
|
let typeNode: SyntaxNode | null = null;
|
||||||
|
|
||||||
if (node.type === 'parameter') {
|
if (node.type === 'parameter') {
|
||||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('internal_name');
|
nameNode = node.childForFieldName('name');
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
} else {
|
} else {
|
||||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
nameNode = node.childForFieldName('name');
|
||||||
typeNode = node.childForFieldName('type');
|
typeNode = node.childForFieldName('type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ const extractInitializer: InitializerExtractor = (
|
||||||
// Skip if has type annotation — extractDeclaration handled it
|
// Skip if has type annotation — extractDeclaration handled it
|
||||||
if (node.childForFieldName('type') || findChild(node, 'type_annotation')) return;
|
if (node.childForFieldName('type') || findChild(node, 'type_annotation')) return;
|
||||||
// Find pattern (variable name)
|
// Find pattern (variable name)
|
||||||
const pattern = node.childForFieldName('pattern') ?? findChild(node, 'pattern');
|
const pattern = findChild(node, 'pattern');
|
||||||
if (!pattern) return;
|
if (!pattern) return;
|
||||||
const varName = extractVarName(pattern) ?? pattern.text;
|
const varName = extractVarName(pattern) ?? pattern.text;
|
||||||
if (!varName || env.has(varName)) return;
|
if (!varName || env.has(varName)) return;
|
||||||
|
|
@ -139,7 +139,7 @@ const extractInitializer: InitializerExtractor = (
|
||||||
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||||
if (node.type !== 'property_declaration') return undefined;
|
if (node.type !== 'property_declaration') return undefined;
|
||||||
if (hasTypeAnnotation(node)) return undefined;
|
if (hasTypeAnnotation(node)) return undefined;
|
||||||
const pattern = node.childForFieldName('pattern') ?? findChild(node, 'pattern');
|
const pattern = findChild(node, 'pattern');
|
||||||
if (!pattern) return undefined;
|
if (!pattern) return undefined;
|
||||||
const varName = pattern.text;
|
const varName = pattern.text;
|
||||||
if (!varName) return undefined;
|
if (!varName) return undefined;
|
||||||
|
|
|
||||||
|
|
@ -300,8 +300,7 @@ const findTsIterableElementType = (
|
||||||
while (current) {
|
while (current) {
|
||||||
if (TS_FUNCTION_NODE_TYPES.has(current.type)) {
|
if (TS_FUNCTION_NODE_TYPES.has(current.type)) {
|
||||||
// Search function parameters
|
// Search function parameters
|
||||||
const paramsNode =
|
const paramsNode = current.childForFieldName('parameters');
|
||||||
current.childForFieldName('parameters') ?? current.childForFieldName('formal_parameters');
|
|
||||||
if (paramsNode) {
|
if (paramsNode) {
|
||||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||||
const param = paramsNode.namedChild(i);
|
const param = paramsNode.namedChild(i);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { SupportedLanguages } from 'gitnexus-shared';
|
import { SupportedLanguages } from 'gitnexus-shared';
|
||||||
import type { VariableExtractionConfig } from '../../variable-types.js';
|
import type { VariableExtractionConfig } from '../../variable-types.js';
|
||||||
import type { VariableVisibility } from '../../variable-types.js';
|
import type { VariableVisibility } from '../../variable-types.js';
|
||||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
|
||||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,13 +46,6 @@ function extractDartVarName(node: SyntaxNode): string | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractDartVarType(node: SyntaxNode): string | undefined {
|
function extractDartVarType(node: SyntaxNode): string | undefined {
|
||||||
for (let i = 0; i < node.namedChildCount; i++) {
|
|
||||||
const child = node.namedChild(i);
|
|
||||||
if (child?.type === 'initialized_variable_definition') {
|
|
||||||
const typeNode = child.childForFieldName('type');
|
|
||||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Look for type_identifier directly on the node
|
// Look for type_identifier directly on the node
|
||||||
for (let i = 0; i < node.namedChildCount; i++) {
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
const child = node.namedChild(i);
|
const child = node.namedChild(i);
|
||||||
|
|
|
||||||
264
gitnexus/test/helpers/grammar-introspection.ts
Normal file
264
gitnexus/test/helpers/grammar-introspection.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
/**
|
||||||
|
* Grammar introspection helper for the tree-sitter node-type / field-name
|
||||||
|
* validation gate (issue #1920).
|
||||||
|
*
|
||||||
|
* Two oracles, layered (see the plan's KTD1):
|
||||||
|
* 1. A fast **membership set** built from each grammar's static
|
||||||
|
* `node-types.json` — the union of every top-level `type`, every
|
||||||
|
* `subtypes[].type`, and every children/per-field `types[].type`,
|
||||||
|
* retaining anonymous (`named:false`) tokens and supertype names.
|
||||||
|
* 2. A `probeNodeType` **authoritative fallback** that compiles a probe
|
||||||
|
* query against the *live* grammar — used for any literal the static
|
||||||
|
* JSON under-reports (regex / `token(...)` tokens, aliased nodes).
|
||||||
|
*
|
||||||
|
* This file lives under `test/` and is therefore allowed to name languages
|
||||||
|
* (the AGENTS.md "shared pipeline code must not name languages" rule applies
|
||||||
|
* to `src/core/ingestion/`, not to test helpers). The live-grammar access and
|
||||||
|
* the tsx/php_only variant handling are delegated to the production
|
||||||
|
* `parser-loader.ts` so the gate validates against exactly the grammar the
|
||||||
|
* runtime uses.
|
||||||
|
*/
|
||||||
|
import Parser from 'tree-sitter';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||||
|
import {
|
||||||
|
getLanguageGrammar,
|
||||||
|
isLanguageAvailable,
|
||||||
|
resolveLanguageKey,
|
||||||
|
} from '../../src/core/tree-sitter/parser-loader.js';
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-language grammar package + the `node-types.json` subpath(s) to union.
|
||||||
|
* COBOL is intentionally absent (regex preprocessor, no grammar). Vue has no
|
||||||
|
* grammar of its own and reuses tree-sitter-typescript, so its literals are
|
||||||
|
* validated against the typescript ∪ tsx node set (JSX/TSX-only nodes
|
||||||
|
* included). The package names mirror `parser-loader.ts` `SOURCES`.
|
||||||
|
*/
|
||||||
|
const GRAMMAR_PACKAGES: Partial<Record<SupportedLanguages, { pkg: string; subpaths: string[] }>> = {
|
||||||
|
[SupportedLanguages.JavaScript]: {
|
||||||
|
pkg: 'tree-sitter-javascript',
|
||||||
|
subpaths: ['src/node-types.json'],
|
||||||
|
},
|
||||||
|
[SupportedLanguages.TypeScript]: {
|
||||||
|
pkg: 'tree-sitter-typescript',
|
||||||
|
subpaths: ['typescript/src/node-types.json', 'tsx/src/node-types.json'],
|
||||||
|
},
|
||||||
|
[SupportedLanguages.Python]: { pkg: 'tree-sitter-python', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Java]: { pkg: 'tree-sitter-java', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.C]: { pkg: 'tree-sitter-c', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.CPlusPlus]: { pkg: 'tree-sitter-cpp', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.CSharp]: { pkg: 'tree-sitter-c-sharp', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Go]: { pkg: 'tree-sitter-go', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Ruby]: { pkg: 'tree-sitter-ruby', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Rust]: { pkg: 'tree-sitter-rust', subpaths: ['src/node-types.json'] },
|
||||||
|
// tree-sitter-php's runtime export is `php_only` (see parser-loader), so the
|
||||||
|
// gate must validate against that variant's node set, not the embedded-HTML
|
||||||
|
// `php` grammar.
|
||||||
|
[SupportedLanguages.PHP]: { pkg: 'tree-sitter-php', subpaths: ['php_only/src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Kotlin]: { pkg: 'tree-sitter-kotlin', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Swift]: { pkg: 'tree-sitter-swift', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Dart]: { pkg: 'tree-sitter-dart', subpaths: ['src/node-types.json'] },
|
||||||
|
[SupportedLanguages.Vue]: {
|
||||||
|
pkg: 'tree-sitter-typescript',
|
||||||
|
subpaths: ['typescript/src/node-types.json', 'tsx/src/node-types.json'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Languages the gate validates (everything with a grammar package). */
|
||||||
|
export const GATED_LANGUAGES: readonly SupportedLanguages[] = Object.keys(
|
||||||
|
GRAMMAR_PACKAGES,
|
||||||
|
) as SupportedLanguages[];
|
||||||
|
|
||||||
|
export interface GrammarModel {
|
||||||
|
language: SupportedLanguages;
|
||||||
|
/** Every node-type string the grammar can surface (named + anonymous + supertypes). */
|
||||||
|
nodeTypes: ReadonlySet<string>;
|
||||||
|
/** Valid field names per node type. */
|
||||||
|
fieldsByNode: ReadonlyMap<string, ReadonlySet<string>>;
|
||||||
|
/** Union of every field name across all node types (sound global existence check). */
|
||||||
|
allFields: ReadonlySet<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- node-types.json shape (only the parts we read) ----
|
||||||
|
interface ChildType {
|
||||||
|
type: string;
|
||||||
|
named: boolean;
|
||||||
|
}
|
||||||
|
interface FieldInfo {
|
||||||
|
types?: ChildType[];
|
||||||
|
}
|
||||||
|
interface NodeTypeEntry {
|
||||||
|
type: string;
|
||||||
|
named?: boolean;
|
||||||
|
fields?: Record<string, FieldInfo>;
|
||||||
|
children?: { types?: ChildType[] };
|
||||||
|
subtypes?: ChildType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve the on-disk directory of an installed package, or null if absent. */
|
||||||
|
function resolvePackageDir(pkg: string): string | null {
|
||||||
|
try {
|
||||||
|
return dirname(_require.resolve(`${pkg}/package.json`));
|
||||||
|
} catch {
|
||||||
|
/* package.json may be blocked by an `exports` map — fall back to main */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let dir = dirname(_require.resolve(pkg));
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (existsSync(join(dir, 'package.json'))) return dir;
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* not installed (optional grammar) */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChildTypes(into: Set<string>, types: ChildType[] | undefined): void {
|
||||||
|
if (!types) return;
|
||||||
|
for (const t of types) into.add(t.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the membership model for one language by unioning its node-types.json
|
||||||
|
* file(s). Returns null when no node-types.json can be resolved (e.g. an
|
||||||
|
* optional grammar is not installed) so callers can skip rather than fail.
|
||||||
|
*/
|
||||||
|
export function loadGrammarModel(language: SupportedLanguages): GrammarModel | null {
|
||||||
|
const entry = GRAMMAR_PACKAGES[language];
|
||||||
|
if (!entry) return null;
|
||||||
|
const dir = resolvePackageDir(entry.pkg);
|
||||||
|
if (!dir) return null;
|
||||||
|
|
||||||
|
const nodeTypes = new Set<string>();
|
||||||
|
const fieldsByNode = new Map<string, Set<string>>();
|
||||||
|
const allFields = new Set<string>();
|
||||||
|
let read = 0;
|
||||||
|
|
||||||
|
for (const subpath of entry.subpaths) {
|
||||||
|
const file = join(dir, subpath);
|
||||||
|
if (!existsSync(file)) continue;
|
||||||
|
let parsed: NodeTypeEntry[];
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(readFileSync(file, 'utf8')) as NodeTypeEntry[];
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
read += 1;
|
||||||
|
for (const node of parsed) {
|
||||||
|
if (typeof node.type === 'string') nodeTypes.add(node.type);
|
||||||
|
addChildTypes(nodeTypes, node.subtypes);
|
||||||
|
addChildTypes(nodeTypes, node.children?.types);
|
||||||
|
if (node.fields) {
|
||||||
|
const fieldSet = fieldsByNode.get(node.type) ?? new Set<string>();
|
||||||
|
for (const [fieldName, info] of Object.entries(node.fields)) {
|
||||||
|
fieldSet.add(fieldName);
|
||||||
|
allFields.add(fieldName);
|
||||||
|
addChildTypes(nodeTypes, info.types);
|
||||||
|
}
|
||||||
|
fieldsByNode.set(node.type, fieldSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read === 0) return null;
|
||||||
|
return { language, nodeTypes, fieldsByNode, allFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the thrown object is tree-sitter's "invalid node type" query error. */
|
||||||
|
export function isNodeTypeError(err: unknown): boolean {
|
||||||
|
return err instanceof Error && /TSQueryErrorNodeType/.test(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Escape a string so it is safe inside a `"..."` anonymous-node query literal. */
|
||||||
|
function escapeAnonymous(literal: string): string {
|
||||||
|
return literal.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The live grammar object(s) a language's literals should be probed against. */
|
||||||
|
function grammarsFor(language: SupportedLanguages): unknown[] {
|
||||||
|
if (!isLanguageAvailable(language)) return [];
|
||||||
|
const grammars: unknown[] = [getLanguageGrammar(language)];
|
||||||
|
// TypeScript and Vue (which reuses the TS grammar) also have a tsx grammar
|
||||||
|
// with JSX-only node types; probe both.
|
||||||
|
if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.Vue) {
|
||||||
|
try {
|
||||||
|
// resolveLanguageKey only switches TypeScript -> tsx on a .tsx path.
|
||||||
|
const tsx = getLanguageGrammar(SupportedLanguages.TypeScript, 'x.tsx');
|
||||||
|
if (resolveLanguageKey(SupportedLanguages.TypeScript, 'x.tsx').endsWith(':tsx')) {
|
||||||
|
grammars.push(tsx);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* tsx unavailable — base grammar still probed */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grammars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authoritative fallback: ask the live grammar whether `literal` can be a node
|
||||||
|
* type. A literal is `valid` if it compiles in ANY of the named `(x)`,
|
||||||
|
* anonymous `"x"`, or supertype `(_x)` forms against ANY of the language's
|
||||||
|
* grammars; `dead` only if every form is rejected; `unavailable` if no grammar
|
||||||
|
* loads (so the caller skips rather than fails). See KTD1.
|
||||||
|
*/
|
||||||
|
export function probeNodeType(
|
||||||
|
language: SupportedLanguages,
|
||||||
|
literal: string,
|
||||||
|
): 'valid' | 'dead' | 'unavailable' {
|
||||||
|
const grammars = grammarsFor(language);
|
||||||
|
if (grammars.length === 0) return 'unavailable';
|
||||||
|
|
||||||
|
const forms = [`(${literal}) @_`, `"${escapeAnonymous(literal)}" @_`, `(_${literal}) @_`];
|
||||||
|
for (const grammar of grammars) {
|
||||||
|
for (const form of forms) {
|
||||||
|
try {
|
||||||
|
// Constructing the Query is the validation: it throws
|
||||||
|
// TSQueryErrorNodeType iff the node type cannot exist.
|
||||||
|
new Parser.Query(grammar as ConstructorParameters<typeof Parser.Query>[0], form);
|
||||||
|
return 'valid';
|
||||||
|
} catch {
|
||||||
|
/* this (form, grammar) rejected — try the next */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'dead';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined check used by the gate: fast membership first, authoritative live
|
||||||
|
* probe only for literals the static JSON does not list. Returns `valid`,
|
||||||
|
* `dead`, or `unavailable`.
|
||||||
|
*/
|
||||||
|
export function validateNodeType(
|
||||||
|
language: SupportedLanguages,
|
||||||
|
model: GrammarModel | null,
|
||||||
|
literal: string,
|
||||||
|
): 'valid' | 'dead' | 'unavailable' {
|
||||||
|
if (model && model.nodeTypes.has(literal)) return 'valid';
|
||||||
|
return probeNodeType(language, literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field-name validation. Node-scoped when the receiver node type is known and
|
||||||
|
* present in the model; otherwise a sound global existence check. Returns
|
||||||
|
* `unavailable` when the model could not be loaded.
|
||||||
|
*/
|
||||||
|
export function validateField(
|
||||||
|
model: GrammarModel | null,
|
||||||
|
field: string,
|
||||||
|
receiverNodeType?: string,
|
||||||
|
): 'valid' | 'dead' | 'unavailable' {
|
||||||
|
if (!model) return 'unavailable';
|
||||||
|
if (receiverNodeType) {
|
||||||
|
const scoped = model.fieldsByNode.get(receiverNodeType);
|
||||||
|
if (scoped) return scoped.has(field) ? 'valid' : 'dead';
|
||||||
|
}
|
||||||
|
return model.allFields.has(field) ? 'valid' : 'dead';
|
||||||
|
}
|
||||||
655
gitnexus/test/helpers/literal-collectors.ts
Normal file
655
gitnexus/test/helpers/literal-collectors.ts
Normal file
|
|
@ -0,0 +1,655 @@
|
||||||
|
/**
|
||||||
|
* Literal collectors for the node-type / field validation gate (issue #1920).
|
||||||
|
*
|
||||||
|
* Collects every tree-sitter node-type and field-name literal the ingestion
|
||||||
|
* layer references in CODE (the query strings themselves are validated by
|
||||||
|
* compilation — see Mode 3 and query-compilation.test.ts), each tagged with
|
||||||
|
* the grammar language(s) it is checked against.
|
||||||
|
*
|
||||||
|
* THREE modes (plan KTD3):
|
||||||
|
* 1. Config reflection — import `*-extractors/configs/*.ts`, read each
|
||||||
|
* config-shaped export's node-type-array keys. Exact `config.language`.
|
||||||
|
* 2. AST scan (`typescript` parser, no type-checker) over the EXTRACTION
|
||||||
|
* surface — `*-extractors/**`, every `languages/<lang>/captures.ts`, and
|
||||||
|
* `export-detection.ts`. Collected BY CONSUMPTION SITE: `<n>.type === '..'`,
|
||||||
|
* `childForFieldName('..')`, `findNodeAtRange(.., '..')`, and members of a
|
||||||
|
* `Set`/array consumed via `SET.has(<n>.type)`. This surface is exactly
|
||||||
|
* the AST-walking code where `.type` is a tree-sitter `SyntaxNode`; the
|
||||||
|
* resolution layer (scope-resolver/interpret/type-env/call-processor —
|
||||||
|
* where `.type` is a resolved-symbol KIND like 'Class') is deliberately
|
||||||
|
* NOT scanned, so semantic-type sets (`PRIMITIVE_TYPES`,
|
||||||
|
* `NULLABLE_WRAPPER_TYPES`) are never mistaken for node types. There is
|
||||||
|
* also no `*_TYPES` name heuristic: a `Set`'s members are collected only
|
||||||
|
* when the set is consumed against a node's `.type`.
|
||||||
|
* 3. Registry scope-query probes — invoke each `languages/<lang>/query.ts`
|
||||||
|
* `get*ScopeQuery()` (gated by `isLanguageAvailable`) so the gate compiles
|
||||||
|
* the registry scope queries too (new coverage vs query-compilation.test.ts).
|
||||||
|
*
|
||||||
|
* Test-only file: allowed to name languages.
|
||||||
|
*/
|
||||||
|
import ts from 'typescript';
|
||||||
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||||
|
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
|
||||||
|
import { GATED_LANGUAGES } from './grammar-introspection.js';
|
||||||
|
|
||||||
|
const INGESTION_DIR = fileURLToPath(new URL('../../src/core/ingestion/', import.meta.url));
|
||||||
|
|
||||||
|
export interface CollectedNodeType {
|
||||||
|
literal: string;
|
||||||
|
languages: SupportedLanguages[];
|
||||||
|
file: string; // ingestion-relative
|
||||||
|
line: number;
|
||||||
|
source: 'config' | 'compare' | 'set-member' | 'find-node-arg';
|
||||||
|
}
|
||||||
|
export interface CollectedField {
|
||||||
|
field: string;
|
||||||
|
languages: SupportedLanguages[];
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
}
|
||||||
|
export interface RegistryQueryProbe {
|
||||||
|
language: SupportedLanguages;
|
||||||
|
getter: string;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_LANGS = GATED_LANGUAGES;
|
||||||
|
|
||||||
|
/** Directory name (under languages/) → language. */
|
||||||
|
const DIR_LANG: Record<string, SupportedLanguages> = {
|
||||||
|
javascript: SupportedLanguages.JavaScript,
|
||||||
|
typescript: SupportedLanguages.TypeScript,
|
||||||
|
python: SupportedLanguages.Python,
|
||||||
|
java: SupportedLanguages.Java,
|
||||||
|
c: SupportedLanguages.C,
|
||||||
|
cpp: SupportedLanguages.CPlusPlus,
|
||||||
|
csharp: SupportedLanguages.CSharp,
|
||||||
|
go: SupportedLanguages.Go,
|
||||||
|
ruby: SupportedLanguages.Ruby,
|
||||||
|
rust: SupportedLanguages.Rust,
|
||||||
|
php: SupportedLanguages.PHP,
|
||||||
|
kotlin: SupportedLanguages.Kotlin,
|
||||||
|
swift: SupportedLanguages.Swift,
|
||||||
|
dart: SupportedLanguages.Dart,
|
||||||
|
vue: SupportedLanguages.Vue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Basename (no .ts) → language set, for extractor files that name a language. */
|
||||||
|
const BASENAME_LANGS: Record<string, SupportedLanguages[]> = {
|
||||||
|
'c-cpp': [SupportedLanguages.C, SupportedLanguages.CPlusPlus],
|
||||||
|
jvm: [SupportedLanguages.Java, SupportedLanguages.Kotlin],
|
||||||
|
'typescript-javascript': [SupportedLanguages.TypeScript, SupportedLanguages.JavaScript],
|
||||||
|
csharp: [SupportedLanguages.CSharp],
|
||||||
|
dart: [SupportedLanguages.Dart],
|
||||||
|
go: [SupportedLanguages.Go],
|
||||||
|
php: [SupportedLanguages.PHP],
|
||||||
|
python: [SupportedLanguages.Python],
|
||||||
|
ruby: [SupportedLanguages.Ruby],
|
||||||
|
rust: [SupportedLanguages.Rust],
|
||||||
|
swift: [SupportedLanguages.Swift],
|
||||||
|
typescript: [SupportedLanguages.TypeScript],
|
||||||
|
javascript: [SupportedLanguages.JavaScript],
|
||||||
|
java: [SupportedLanguages.Java],
|
||||||
|
kotlin: [SupportedLanguages.Kotlin],
|
||||||
|
laravel: [SupportedLanguages.PHP],
|
||||||
|
nextjs: [SupportedLanguages.TypeScript, SupportedLanguages.JavaScript],
|
||||||
|
expo: [SupportedLanguages.TypeScript, SupportedLanguages.JavaScript],
|
||||||
|
'fastapi-router-bindings': [SupportedLanguages.Python],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** const-name prefix → language (for export-detection.ts style named sets). */
|
||||||
|
const PREFIX_LANGS: Record<string, SupportedLanguages[]> = {
|
||||||
|
CSHARP: [SupportedLanguages.CSharp],
|
||||||
|
RUST: [SupportedLanguages.Rust],
|
||||||
|
GO: [SupportedLanguages.Go],
|
||||||
|
JAVA: [SupportedLanguages.Java],
|
||||||
|
KOTLIN: [SupportedLanguages.Kotlin],
|
||||||
|
PYTHON: [SupportedLanguages.Python],
|
||||||
|
RUBY: [SupportedLanguages.Ruby],
|
||||||
|
PHP: [SupportedLanguages.PHP],
|
||||||
|
SWIFT: [SupportedLanguages.Swift],
|
||||||
|
DART: [SupportedLanguages.Dart],
|
||||||
|
CPP: [SupportedLanguages.CPlusPlus],
|
||||||
|
TS: [SupportedLanguages.TypeScript],
|
||||||
|
JS: [SupportedLanguages.JavaScript],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Candidate grammar languages a CODE literal in `relPath` should be checked against. */
|
||||||
|
function fileLanguages(relPath: string): SupportedLanguages[] {
|
||||||
|
const langsMatch = relPath.match(/(?:^|\/)languages\/([^/]+)\//);
|
||||||
|
if (langsMatch) {
|
||||||
|
const lang = DIR_LANG[langsMatch[1]];
|
||||||
|
return lang ? [lang] : [...ALL_LANGS];
|
||||||
|
}
|
||||||
|
const base = relPath.replace(/\.ts$/, '').split('/').pop() ?? '';
|
||||||
|
if (BASENAME_LANGS[base]) return BASENAME_LANGS[base];
|
||||||
|
// generic / shared / cross-language helpers → any grammar (valid-if-any)
|
||||||
|
return [...ALL_LANGS];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Narrow a Set's candidate languages by a `<LANG>_...` const-name prefix. */
|
||||||
|
function constNameLanguages(
|
||||||
|
constName: string,
|
||||||
|
fallback: SupportedLanguages[],
|
||||||
|
): SupportedLanguages[] {
|
||||||
|
const m = constName.match(/^([A-Z]+)_/);
|
||||||
|
if (m && PREFIX_LANGS[m[1]]) return PREFIX_LANGS[m[1]];
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// File discovery
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function walkTs(dir: string, out: string[]): void {
|
||||||
|
if (!existsSync(dir)) return;
|
||||||
|
for (const entry of readdirSync(dir)) {
|
||||||
|
const full = join(dir, entry);
|
||||||
|
const st = statSync(full);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
walkTs(full, out);
|
||||||
|
} else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts')) {
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Mode-2 scan surface: every *-extractors/** file + each captures.ts + export-detection.ts. */
|
||||||
|
function mode2Files(): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const entry of readdirSync(INGESTION_DIR)) {
|
||||||
|
if (entry.endsWith('-extractors')) walkTs(join(INGESTION_DIR, entry), files);
|
||||||
|
}
|
||||||
|
const langsDir = join(INGESTION_DIR, 'languages');
|
||||||
|
if (existsSync(langsDir)) {
|
||||||
|
for (const lang of readdirSync(langsDir)) {
|
||||||
|
if (lang === 'cobol') continue;
|
||||||
|
const cap = join(langsDir, lang, 'captures.ts');
|
||||||
|
if (existsSync(cap)) files.push(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const exportDetection = join(INGESTION_DIR, 'export-detection.ts');
|
||||||
|
if (existsSync(exportDetection)) files.push(exportDetection);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The config files for Mode-1 reflection. */
|
||||||
|
function configFiles(): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const entry of readdirSync(INGESTION_DIR)) {
|
||||||
|
if (!entry.endsWith('-extractors')) continue;
|
||||||
|
const cfgDir = join(INGESTION_DIR, entry, 'configs');
|
||||||
|
if (existsSync(cfgDir)) walkTs(cfgDir, files);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rel = (abs: string): string => abs.slice(INGESTION_DIR.length);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mode 1 — config reflection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const CONFIG_NODE_TYPE_KEYS = new Set([
|
||||||
|
'typeDeclarationNodes',
|
||||||
|
'methodNodeTypes',
|
||||||
|
'bodyNodeTypes',
|
||||||
|
'fieldNodeTypes',
|
||||||
|
'variableNodeTypes',
|
||||||
|
'staticNodeTypes',
|
||||||
|
'constNodeTypes',
|
||||||
|
'ancestorScopeNodeTypes',
|
||||||
|
'fileScopeNodeTypes',
|
||||||
|
'enumNodeTypes',
|
||||||
|
'propertyNodeTypes',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isStringArray = (v: unknown): v is string[] =>
|
||||||
|
Array.isArray(v) && v.every((x) => typeof x === 'string');
|
||||||
|
|
||||||
|
async function collectConfigNodeTypes(): Promise<CollectedNodeType[]> {
|
||||||
|
const out: CollectedNodeType[] = [];
|
||||||
|
for (const file of configFiles()) {
|
||||||
|
const relPath = rel(file);
|
||||||
|
let mod: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
// import the compiled .js sibling (vitest transpiles src on import)
|
||||||
|
mod = (await import(file)) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const exported of Object.values(mod)) {
|
||||||
|
if (!exported || typeof exported !== 'object') continue;
|
||||||
|
const cfg = exported as Record<string, unknown>;
|
||||||
|
const lang = cfg.language;
|
||||||
|
if (typeof lang !== 'string' || !ALL_LANGS.includes(lang as SupportedLanguages)) continue;
|
||||||
|
// Tag by the config FILE's served language set, not the single config
|
||||||
|
// object's `.language`: a shared file (typescript-javascript, c-cpp, jvm)
|
||||||
|
// legitimately lists nodes valid in a sibling grammar, so a node valid in
|
||||||
|
// ANY served language must not be flagged dead. Union the object's own
|
||||||
|
// language in case the file map is broader/narrower.
|
||||||
|
const fileLangs = fileLanguages(relPath);
|
||||||
|
const languages = fileLangs.includes(lang as SupportedLanguages)
|
||||||
|
? fileLangs
|
||||||
|
: [...fileLangs, lang as SupportedLanguages];
|
||||||
|
for (const [key, value] of Object.entries(cfg)) {
|
||||||
|
if (!CONFIG_NODE_TYPE_KEYS.has(key) || !isStringArray(value)) continue;
|
||||||
|
for (const literal of value) {
|
||||||
|
out.push({ literal, languages, file: relPath, line: 0, source: 'config' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mode 2 — AST scan
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const FIELD_LOOKUP_NAMES = new Set(['childForFieldName', 'childrenForFieldName']);
|
||||||
|
const MEMBERSHIP_NAMES = new Set(['has', 'includes']);
|
||||||
|
|
||||||
|
/** Is `node` a `<expr>.type` property access? */
|
||||||
|
function isDotType(node: ts.Node): node is ts.PropertyAccessExpression {
|
||||||
|
return ts.isPropertyAccessExpression(node) && node.name.text === 'type';
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineOf(sf: ts.SourceFile, node: ts.Node): number {
|
||||||
|
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mode2Result {
|
||||||
|
nodeTypes: CollectedNodeType[];
|
||||||
|
fields: CollectedField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract string members of `new Set([...])` / `[...]` / `[...] as const`, or null if not a literal string array. */
|
||||||
|
function collectConstMembers(init: ts.Expression): string[] | null {
|
||||||
|
let arr: ts.Expression | undefined;
|
||||||
|
if (ts.isNewExpression(init) && init.arguments && init.arguments.length > 0) {
|
||||||
|
arr = init.arguments[0];
|
||||||
|
} else if (ts.isArrayLiteralExpression(init)) {
|
||||||
|
arr = init;
|
||||||
|
} else if (ts.isAsExpression(init)) {
|
||||||
|
return collectConstMembers(init.expression);
|
||||||
|
}
|
||||||
|
if (arr && ts.isArrayLiteralExpression(arr)) {
|
||||||
|
const members = arr.elements
|
||||||
|
.filter((e): e is ts.StringLiteral => ts.isStringLiteral(e))
|
||||||
|
.map((e) => e.text);
|
||||||
|
return members.length === arr.elements.length ? members : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanFile(file: string): Mode2Result {
|
||||||
|
const relPath = rel(file);
|
||||||
|
const langs = fileLanguages(relPath);
|
||||||
|
const src = readFileSync(file, 'utf8');
|
||||||
|
const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true);
|
||||||
|
const nodeTypes: CollectedNodeType[] = [];
|
||||||
|
const fields: CollectedField[] = [];
|
||||||
|
|
||||||
|
// First pass: index module-level string Set/array consts, and record which
|
||||||
|
// const identifiers are consumed via `SET.has(<n>.type)` / `.includes(<n>.type)`.
|
||||||
|
const constMembers = new Map<string, string[]>();
|
||||||
|
const typeConsumed = new Set<string>();
|
||||||
|
|
||||||
|
const visit = (node: ts.Node): void => {
|
||||||
|
// module-level const Set/array of strings
|
||||||
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||||
|
const members = collectConstMembers(node.initializer);
|
||||||
|
if (members) constMembers.set(node.name.text, members);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `<n>.type === 'lit'` / `!==`
|
||||||
|
if (
|
||||||
|
ts.isBinaryExpression(node) &&
|
||||||
|
(node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||||
|
node.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
|
||||||
|
) {
|
||||||
|
const { left, right } = node;
|
||||||
|
const lit = ts.isStringLiteral(left) ? left : ts.isStringLiteral(right) ? right : null;
|
||||||
|
const dot = isDotType(left) ? left : isDotType(right) ? right : null;
|
||||||
|
if (lit && dot) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal: lit.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, lit),
|
||||||
|
source: 'compare',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
||||||
|
const method = node.expression.name.text;
|
||||||
|
const arg0 = node.arguments[0];
|
||||||
|
// childForFieldName('field')
|
||||||
|
if (FIELD_LOOKUP_NAMES.has(method) && arg0 && ts.isStringLiteral(arg0)) {
|
||||||
|
fields.push({
|
||||||
|
field: arg0.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, arg0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// SET.has(<n>.type) / SET.includes(<n>.type) → mark the receiver set
|
||||||
|
if (
|
||||||
|
MEMBERSHIP_NAMES.has(method) &&
|
||||||
|
arg0 &&
|
||||||
|
isDotType(arg0) &&
|
||||||
|
ts.isIdentifier(node.expression.expression)
|
||||||
|
) {
|
||||||
|
typeConsumed.add(node.expression.expression.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNodeAtRange(a, b, 'lit') — 3rd arg, literal only (skip dynamic)
|
||||||
|
if (
|
||||||
|
ts.isCallExpression(node) &&
|
||||||
|
((ts.isIdentifier(node.expression) && node.expression.text === 'findNodeAtRange') ||
|
||||||
|
(ts.isPropertyAccessExpression(node.expression) &&
|
||||||
|
node.expression.name.text === 'findNodeAtRange'))
|
||||||
|
) {
|
||||||
|
const a2 = node.arguments[2];
|
||||||
|
if (a2 && ts.isStringLiteral(a2)) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal: a2.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, a2),
|
||||||
|
source: 'find-node-arg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
};
|
||||||
|
visit(sf);
|
||||||
|
|
||||||
|
// Second pass: emit members of every set that was consumed against `.type`.
|
||||||
|
for (const constName of typeConsumed) {
|
||||||
|
const members = constMembers.get(constName);
|
||||||
|
if (!members) continue; // imported or non-literal set — skip (sound: don't guess)
|
||||||
|
const memberLangs = constNameLanguages(constName, langs);
|
||||||
|
for (const literal of members) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal,
|
||||||
|
languages: memberLangs,
|
||||||
|
file: relPath,
|
||||||
|
line: 0,
|
||||||
|
source: 'set-member',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodeTypes, fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectInCodeLiterals(): Mode2Result {
|
||||||
|
const nodeTypes: CollectedNodeType[] = [];
|
||||||
|
const fields: CollectedField[] = [];
|
||||||
|
for (const file of mode2Files()) {
|
||||||
|
const r = scanFile(file);
|
||||||
|
nodeTypes.push(...r.nodeTypes);
|
||||||
|
fields.push(...r.fields);
|
||||||
|
}
|
||||||
|
return { nodeTypes, fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mode 4 — registry RESOLUTION layer (scope-resolver/type-binding/receiver-
|
||||||
|
// binding/interpret/arity/import-decomposer/...), the production path for
|
||||||
|
// migrated languages. These files mix SyntaxNode `.type` (grammar nodes) with
|
||||||
|
// resolved-symbol `.type` (kinds like 'Class'); a naive scan would false-
|
||||||
|
// positive on the latter. So this mode uses the TS TypeChecker to collect a
|
||||||
|
// literal ONLY when its `.type` receiver / childForFieldName target resolves to
|
||||||
|
// a tree-sitter SyntaxNode. Per-language dir => grammar (no cross-lang ambiguity).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
||||||
|
const RES_SKIP = new Set(['captures.ts', 'query.ts', 'index.ts']);
|
||||||
|
const NODE_ARG_FNS = new Set(['findChild', 'findNamedChild', 'findSiblingChild']);
|
||||||
|
|
||||||
|
function resolutionLayerFiles(): { file: string; lang: SupportedLanguages }[] {
|
||||||
|
const out: { file: string; lang: SupportedLanguages }[] = [];
|
||||||
|
const langsDir = join(INGESTION_DIR, 'languages');
|
||||||
|
if (!existsSync(langsDir)) return out;
|
||||||
|
for (const dir of readdirSync(langsDir)) {
|
||||||
|
if (dir === 'cobol') continue;
|
||||||
|
const lang = DIR_LANG[dir];
|
||||||
|
if (!lang) continue;
|
||||||
|
const d = join(langsDir, dir);
|
||||||
|
if (!statSync(d).isDirectory()) continue;
|
||||||
|
const sub: string[] = [];
|
||||||
|
walkTs(d, sub);
|
||||||
|
for (const f of sub) {
|
||||||
|
if (!RES_SKIP.has(f.split('/').pop() ?? '')) out.push({ file: f, lang });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _program: ts.Program | null = null;
|
||||||
|
let _checker: ts.TypeChecker | null = null;
|
||||||
|
function buildProgram(
|
||||||
|
rootFiles: string[],
|
||||||
|
): { program: ts.Program; checker: ts.TypeChecker } | null {
|
||||||
|
if (_program && _checker) return { program: _program, checker: _checker };
|
||||||
|
try {
|
||||||
|
const cfg = ts.readConfigFile(join(REPO_ROOT, 'tsconfig.json'), ts.sys.readFile);
|
||||||
|
const parsed = ts.parseJsonConfigFileContent(cfg.config ?? {}, ts.sys, REPO_ROOT);
|
||||||
|
const options: ts.CompilerOptions = { ...parsed.options, noEmit: true, skipLibCheck: true };
|
||||||
|
_program = ts.createProgram(rootFiles, options);
|
||||||
|
_checker = _program.getTypeChecker();
|
||||||
|
return { program: _program, checker: _checker };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when `node`'s resolved type is (or includes) a tree-sitter SyntaxNode. */
|
||||||
|
function isSyntaxNodeReceiver(checker: ts.TypeChecker, node: ts.Node): boolean {
|
||||||
|
try {
|
||||||
|
const s = checker.typeToString(checker.getTypeAtLocation(node));
|
||||||
|
return /\bSyntaxNode\b/.test(s);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Did the build succeed? (false => mode degraded; surfaced so coverage isn't silently lost) */
|
||||||
|
export let resolutionLayerProgramOk = true;
|
||||||
|
|
||||||
|
function collectResolutionLayerLiterals(): Mode2Result {
|
||||||
|
const nodeTypes: CollectedNodeType[] = [];
|
||||||
|
const fields: CollectedField[] = [];
|
||||||
|
const entries = resolutionLayerFiles();
|
||||||
|
const built = buildProgram(entries.map((e) => e.file));
|
||||||
|
if (!built) {
|
||||||
|
resolutionLayerProgramOk = false;
|
||||||
|
return { nodeTypes, fields };
|
||||||
|
}
|
||||||
|
const { program, checker } = built;
|
||||||
|
|
||||||
|
for (const { file, lang } of entries) {
|
||||||
|
const sf = program.getSourceFile(file);
|
||||||
|
if (!sf) continue;
|
||||||
|
const relPath = rel(file);
|
||||||
|
const langs = [lang];
|
||||||
|
const constMembers = new Map<string, string[]>();
|
||||||
|
const consumedSets = new Set<string>();
|
||||||
|
|
||||||
|
const visit = (node: ts.Node): void => {
|
||||||
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||||
|
const m = collectConstMembers(node.initializer);
|
||||||
|
if (m) constMembers.set(node.name.text, m);
|
||||||
|
}
|
||||||
|
// `<recv>.type === 'lit'` — only when recv is a SyntaxNode
|
||||||
|
if (
|
||||||
|
ts.isBinaryExpression(node) &&
|
||||||
|
(node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||||
|
node.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
|
||||||
|
) {
|
||||||
|
const { left, right } = node;
|
||||||
|
const lit = ts.isStringLiteral(left) ? left : ts.isStringLiteral(right) ? right : null;
|
||||||
|
const dot = isDotType(left) ? left : isDotType(right) ? right : null;
|
||||||
|
if (lit && dot && isSyntaxNodeReceiver(checker, dot.expression)) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal: lit.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, lit),
|
||||||
|
source: 'compare',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
||||||
|
const method = node.expression.name.text;
|
||||||
|
const arg0 = node.arguments[0];
|
||||||
|
// childForFieldName('field') on a SyntaxNode
|
||||||
|
if (
|
||||||
|
FIELD_LOOKUP_NAMES.has(method) &&
|
||||||
|
arg0 &&
|
||||||
|
ts.isStringLiteral(arg0) &&
|
||||||
|
isSyntaxNodeReceiver(checker, node.expression.expression)
|
||||||
|
) {
|
||||||
|
fields.push({
|
||||||
|
field: arg0.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, arg0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// SET.has(<recv>.type) where recv is a SyntaxNode
|
||||||
|
if (
|
||||||
|
MEMBERSHIP_NAMES.has(method) &&
|
||||||
|
arg0 &&
|
||||||
|
isDotType(arg0) &&
|
||||||
|
ts.isIdentifier(node.expression.expression) &&
|
||||||
|
isSyntaxNodeReceiver(checker, arg0.expression)
|
||||||
|
) {
|
||||||
|
consumedSets.add(node.expression.expression.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// findChild/findNamedChild/findSiblingChild(<recv>, 'lit') 2nd arg, or
|
||||||
|
// findNodeAtRange(_, _, 'lit') 3rd arg — node-type literals; gate recv.
|
||||||
|
if (ts.isCallExpression(node)) {
|
||||||
|
const callee = node.expression;
|
||||||
|
const fname = ts.isIdentifier(callee)
|
||||||
|
? callee.text
|
||||||
|
: ts.isPropertyAccessExpression(callee)
|
||||||
|
? callee.name.text
|
||||||
|
: '';
|
||||||
|
if (NODE_ARG_FNS.has(fname)) {
|
||||||
|
const recv = node.arguments[0];
|
||||||
|
const a1 = node.arguments[1];
|
||||||
|
if (a1 && ts.isStringLiteral(a1) && recv && isSyntaxNodeReceiver(checker, recv)) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal: a1.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, a1),
|
||||||
|
source: 'find-node-arg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (fname === 'findNodeAtRange') {
|
||||||
|
const a2 = node.arguments[2];
|
||||||
|
if (a2 && ts.isStringLiteral(a2)) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal: a2.text,
|
||||||
|
languages: langs,
|
||||||
|
file: relPath,
|
||||||
|
line: lineOf(sf, a2),
|
||||||
|
source: 'find-node-arg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
};
|
||||||
|
visit(sf);
|
||||||
|
|
||||||
|
for (const constName of consumedSets) {
|
||||||
|
const members = constMembers.get(constName);
|
||||||
|
if (!members) continue;
|
||||||
|
for (const literal of members) {
|
||||||
|
nodeTypes.push({
|
||||||
|
literal,
|
||||||
|
languages: constNameLanguages(constName, langs),
|
||||||
|
file: relPath,
|
||||||
|
line: 0,
|
||||||
|
source: 'set-member',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { nodeTypes, fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mode 3 — registry scope-query probes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function collectRegistryQueryProbes(): Promise<RegistryQueryProbe[]> {
|
||||||
|
const out: RegistryQueryProbe[] = [];
|
||||||
|
const langsDir = join(INGESTION_DIR, 'languages');
|
||||||
|
if (!existsSync(langsDir)) return out;
|
||||||
|
for (const dir of readdirSync(langsDir)) {
|
||||||
|
if (dir === 'cobol') continue;
|
||||||
|
const lang = DIR_LANG[dir];
|
||||||
|
if (!lang) continue;
|
||||||
|
const queryFile = join(langsDir, dir, 'query.ts');
|
||||||
|
if (!existsSync(queryFile)) continue;
|
||||||
|
// Importing query.ts loads the grammar at module top level — gate it.
|
||||||
|
if (!isLanguageAvailable(lang)) continue;
|
||||||
|
let mod: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
mod = (await import(queryFile)) as Record<string, unknown>;
|
||||||
|
} catch (e) {
|
||||||
|
out.push({ language: lang, getter: '(import)', error: String((e as Error).message ?? e) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const [name, value] of Object.entries(mod)) {
|
||||||
|
if (typeof value !== 'function' || !/ScopeQuery$/.test(name)) continue;
|
||||||
|
try {
|
||||||
|
(value as () => unknown)();
|
||||||
|
out.push({ language: lang, getter: name, error: null });
|
||||||
|
} catch (e) {
|
||||||
|
out.push({ language: lang, getter: name, error: String((e as Error).message ?? e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public entry point
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export interface CollectedLiterals {
|
||||||
|
nodeTypes: CollectedNodeType[];
|
||||||
|
fields: CollectedField[];
|
||||||
|
queryProbes: RegistryQueryProbe[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectAllLiterals(): Promise<CollectedLiterals> {
|
||||||
|
const config = await collectConfigNodeTypes();
|
||||||
|
const inCode = collectInCodeLiterals();
|
||||||
|
const resolution = collectResolutionLayerLiterals(); // Mode 4 (TypeChecker-gated)
|
||||||
|
const queryProbes = await collectRegistryQueryProbes();
|
||||||
|
return {
|
||||||
|
nodeTypes: [...config, ...inCode.nodeTypes, ...resolution.nodeTypes],
|
||||||
|
fields: [...inCode.fields, ...resolution.fields],
|
||||||
|
queryProbes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposed for focused unit tests.
|
||||||
|
export const __test = {
|
||||||
|
collectConfigNodeTypes,
|
||||||
|
collectInCodeLiterals,
|
||||||
|
collectResolutionLayerLiterals,
|
||||||
|
resolutionLayerFiles,
|
||||||
|
mode2Files,
|
||||||
|
fileLanguages,
|
||||||
|
};
|
||||||
114
gitnexus/test/integration/grammar-introspection.test.ts
Normal file
114
gitnexus/test/integration/grammar-introspection.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
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,
|
||||||
|
validateNodeType,
|
||||||
|
validateField,
|
||||||
|
isNodeTypeError,
|
||||||
|
} 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('returns unavailable (not throw) when a grammar cannot load', () => {
|
||||||
|
// Drive through validateNodeType with a null model for an unavailable lang.
|
||||||
|
// For installed langs this still must not throw.
|
||||||
|
for (const lang of GATED_LANGUAGES) {
|
||||||
|
expect(() => probeNodeType(lang, 'definitely_not_a_node_type_xyz')).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
146
gitnexus/test/integration/grammar-literal-validation.test.ts
Normal file
146
gitnexus/test/integration/grammar-literal-validation.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||||
|
import {
|
||||||
|
GATED_LANGUAGES,
|
||||||
|
loadGrammarModel,
|
||||||
|
validateNodeType,
|
||||||
|
validateField,
|
||||||
|
type GrammarModel,
|
||||||
|
} from '../helpers/grammar-introspection.js';
|
||||||
|
import { collectAllLiterals, type CollectedLiterals } from '../helpers/literal-collectors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grammar-drift gate (issue #1920): every tree-sitter node-type and field-name
|
||||||
|
* literal referenced in the ingestion CODE must be emittable by at least one of
|
||||||
|
* the grammar(s) that code path serves. A literal absent from every candidate
|
||||||
|
* grammar is a "dead branch keyed on a node type the grammar never emits" —
|
||||||
|
* the systemic defect this gate kills.
|
||||||
|
*
|
||||||
|
* Complements query-compilation.test.ts (which compiles the legacy *_QUERIES
|
||||||
|
* banks): this gate covers the NON-compiled literal surface (node.type ===,
|
||||||
|
* childForFieldName, Set/array node-type lists) plus the registry scope queries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Empty by design: every dead grammar literal this gate surfaces is removed in
|
||||||
|
// this PR — no allowlisted debt. Mirrors query-compilation.test.ts:40. Keep it
|
||||||
|
// empty; fix the literal at its source rather than allowlisting it here.
|
||||||
|
const knownFailures = new Set<string>([]);
|
||||||
|
|
||||||
|
interface Failure {
|
||||||
|
kind: 'node-type' | 'field' | 'query';
|
||||||
|
literal: string;
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
languages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = (f: Failure): string =>
|
||||||
|
`${f.kind} "${f.literal}" — ${f.file}:${f.line} — not valid in [${f.languages.join(', ')}]`;
|
||||||
|
|
||||||
|
describe('grammar literal validation gate', () => {
|
||||||
|
let collected: CollectedLiterals;
|
||||||
|
const models = new Map<SupportedLanguages, GrammarModel | null>();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
for (const lang of GATED_LANGUAGES) models.set(lang, loadGrammarModel(lang));
|
||||||
|
collected = await collectAllLiterals();
|
||||||
|
}, 120_000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "valid" if ANY candidate grammar accepts it; "dead" if at least one
|
||||||
|
* candidate rejects it and none accept; "unavailable" if every candidate
|
||||||
|
* grammar is absent (so we skip rather than fail — R9).
|
||||||
|
*/
|
||||||
|
function classify(
|
||||||
|
languages: SupportedLanguages[],
|
||||||
|
check: (lang: SupportedLanguages) => 'valid' | 'dead' | 'unavailable',
|
||||||
|
): 'valid' | 'dead' | 'unavailable' {
|
||||||
|
let sawDead = false;
|
||||||
|
for (const lang of languages) {
|
||||||
|
const r = check(lang);
|
||||||
|
if (r === 'valid') return 'valid';
|
||||||
|
if (r === 'dead') sawDead = true;
|
||||||
|
}
|
||||||
|
return sawDead ? 'dead' : 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
it('every node-type and field literal exists in its grammar; registry queries compile', () => {
|
||||||
|
const failures: Failure[] = [];
|
||||||
|
|
||||||
|
for (const n of collected.nodeTypes) {
|
||||||
|
if (knownFailures.has(n.literal)) continue;
|
||||||
|
const verdict = classify(n.languages, (lang) =>
|
||||||
|
validateNodeType(lang, models.get(lang) ?? null, n.literal),
|
||||||
|
);
|
||||||
|
if (verdict === 'dead') {
|
||||||
|
failures.push({
|
||||||
|
kind: 'node-type',
|
||||||
|
literal: n.literal,
|
||||||
|
file: n.file,
|
||||||
|
line: n.line,
|
||||||
|
languages: n.languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of collected.fields) {
|
||||||
|
if (knownFailures.has(f.field)) continue;
|
||||||
|
const verdict = classify(f.languages, (lang) =>
|
||||||
|
validateField(models.get(lang) ?? null, f.field),
|
||||||
|
);
|
||||||
|
if (verdict === 'dead') {
|
||||||
|
failures.push({
|
||||||
|
kind: 'field',
|
||||||
|
literal: f.field,
|
||||||
|
file: f.file,
|
||||||
|
line: f.line,
|
||||||
|
languages: f.languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const q of collected.queryProbes) {
|
||||||
|
if (q.error) {
|
||||||
|
failures.push({
|
||||||
|
kind: 'query',
|
||||||
|
literal: `${q.getter} (${q.error})`,
|
||||||
|
file: `languages/${q.language}/query.ts`,
|
||||||
|
line: 0,
|
||||||
|
languages: [q.language],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-dup identical (kind, literal, file) rows for a readable report.
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const unique = failures.filter((f) => {
|
||||||
|
const k = `${f.kind}|${f.literal}|${f.file}`;
|
||||||
|
if (seen.has(k)) return false;
|
||||||
|
seen.add(k);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const report =
|
||||||
|
unique.length === 0
|
||||||
|
? ''
|
||||||
|
: `\n${unique.length} dead grammar literal(s) found:\n` +
|
||||||
|
unique
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.file.localeCompare(b.file))
|
||||||
|
.map((f) => ` - ${fmt(f)}`)
|
||||||
|
.join('\n') +
|
||||||
|
'\n';
|
||||||
|
|
||||||
|
expect(unique, report).toHaveLength(0);
|
||||||
|
}, 120_000);
|
||||||
|
|
||||||
|
it('does not flag capture-tag strings', () => {
|
||||||
|
expect(collected.nodeTypes.some((n) => n.literal.startsWith('@'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates a real node-scoped field and rejects a bogus one (Python)', () => {
|
||||||
|
const model = loadGrammarModel(SupportedLanguages.Python);
|
||||||
|
expect(validateField(model, 'name', 'function_definition')).toBe('valid');
|
||||||
|
expect(validateField(model, 'definitely_not_a_field', 'function_definition')).toBe('dead');
|
||||||
|
});
|
||||||
|
});
|
||||||
95
gitnexus/test/integration/literal-collectors.test.ts
Normal file
95
gitnexus/test/integration/literal-collectors.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||||
|
import {
|
||||||
|
collectAllLiterals,
|
||||||
|
__test,
|
||||||
|
type CollectedNodeType,
|
||||||
|
} from '../helpers/literal-collectors.js';
|
||||||
|
|
||||||
|
const hasNodeType = (
|
||||||
|
list: CollectedNodeType[],
|
||||||
|
literal: string,
|
||||||
|
lang?: SupportedLanguages,
|
||||||
|
): boolean =>
|
||||||
|
list.some((n) => n.literal === literal && (lang === undefined || n.languages.includes(lang)));
|
||||||
|
|
||||||
|
describe('literal-collectors', () => {
|
||||||
|
describe('Mode 1 — config reflection', () => {
|
||||||
|
it('splits c-cpp configs by language', async () => {
|
||||||
|
const { nodeTypes } = await collectAllLiterals();
|
||||||
|
const config = nodeTypes.filter((n) => n.source === 'config');
|
||||||
|
expect(hasNodeType(config, 'struct_specifier', SupportedLanguages.C)).toBe(true);
|
||||||
|
expect(hasNodeType(config, 'class_specifier', SupportedLanguages.CPlusPlus)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mode 2 — AST scan over the extraction surface', () => {
|
||||||
|
const { nodeTypes, fields } = __test.collectInCodeLiterals();
|
||||||
|
|
||||||
|
it('collects literals that live OUTSIDE configs/ (the surface fix)', () => {
|
||||||
|
// direct `.type ===` in a single-language type-extractor (a valid, kept literal)
|
||||||
|
expect(hasNodeType(nodeTypes, 'call_expression', SupportedLanguages.Rust)).toBe(true);
|
||||||
|
// a literal inside a per-language captures.ts (valid, kept)
|
||||||
|
expect(hasNodeType(nodeTypes, 'reference_declarator', SupportedLanguages.CPlusPlus)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collects members of a Set consumed against node.type (RUBY_METHOD_NODE_TYPES)', () => {
|
||||||
|
const setMembers = nodeTypes.filter((n) => n.source === 'set-member');
|
||||||
|
expect(hasNodeType(setMembers, 'singleton_method', SupportedLanguages.Ruby)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collects export-detection.ts language-named set members tagged by const prefix', () => {
|
||||||
|
// CSHARP_DECL_TYPES is consumed via `.has(node.type)`; a valid, kept member
|
||||||
|
expect(hasNodeType(nodeTypes, 'record_declaration', SupportedLanguages.CSharp)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('B1 guard: semantic type-name sets are NOT collected as node types', () => {
|
||||||
|
// PRIMITIVE_TYPES / NULLABLE_WRAPPER_TYPES are consumed via .has(text) /
|
||||||
|
// .has(name), never .has(node.type), so their members must never appear.
|
||||||
|
expect(hasNodeType(nodeTypes, 'i32')).toBe(false);
|
||||||
|
expect(hasNodeType(nodeTypes, 'usize')).toBe(false);
|
||||||
|
expect(hasNodeType(nodeTypes, 'Optional')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collects field literals and never collects capture-tag strings as node types', () => {
|
||||||
|
expect(fields.length).toBeGreaterThan(0);
|
||||||
|
// capture tags start with '@' and are compared by name/role, never as node types
|
||||||
|
expect(nodeTypes.some((n) => n.literal.startsWith('@'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not scan the COBOL or resolution layer', () => {
|
||||||
|
expect(nodeTypes.some((n) => n.file.includes('cobol'))).toBe(false);
|
||||||
|
// resolution-layer files (where .type is a resolved-symbol kind) are excluded
|
||||||
|
expect(nodeTypes.some((n) => n.file.endsWith('call-processor.ts'))).toBe(false);
|
||||||
|
expect(nodeTypes.some((n) => n.file.endsWith('type-env.ts'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mode 4 — registry resolution layer (TypeChecker-gated)', () => {
|
||||||
|
it('scans the resolution layer and tags literals by language dir', () => {
|
||||||
|
const { nodeTypes } = __test.collectResolutionLayerLiterals();
|
||||||
|
// The TS Program must have built (else coverage is silently lost).
|
||||||
|
expect(nodeTypes.length).toBeGreaterThan(0);
|
||||||
|
// a real cpp resolution-layer node type (arity-metadata.ts) tagged C++
|
||||||
|
expect(hasNodeType(nodeTypes, 'parameter_declaration', SupportedLanguages.CPlusPlus)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// discriminator: resolution-layer literals are grammar nodes (snake_case /
|
||||||
|
// anonymous), never resolved-symbol PascalCase kinds like 'Class'/'Struct'.
|
||||||
|
expect(nodeTypes.some((n) => /^[A-Z]/.test(n.literal))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mode 3 — registry scope-query probes', () => {
|
||||||
|
it('probes available languages registry scope queries', async () => {
|
||||||
|
const { queryProbes } = await collectAllLiterals();
|
||||||
|
expect(queryProbes.length).toBeGreaterThan(0);
|
||||||
|
// every probe carries a language + getter name
|
||||||
|
for (const p of queryProbes) {
|
||||||
|
expect(p.getter).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue