mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(zig): callable-value-flow captures + main's per-language conformance gates
Post-rebase catch-up: since this branch forked, main added three "every registered language must appear here" tests. Each needs a Zig entry: - callable-value-flow (#2522): Zig now emits `@callable-flow.*` facts via `synthesizeCallableFlowCaptures` (ZIG_CALLABLE_CAPTURE_OPTIONS in zig/captures.ts). tree-sitter-zig's `call_expression` carries arguments as direct children with no wrapper node, which the shared helper could not decompose, so this adds a language-neutral `extractCallArguments` hook (mirror of `extractFunctionParameters`; `undefined` = shared path). Zig joins the provider matrix as 'matrix' with a real assign→copy→ argument→invoke case. - external-import-conformance (#2953): `@import("std")` beside a decoy `src/std.zig` resolves to nothing; the decoy stays reachable via the relative spelling. Zig holds the property (no suffix fallback), so it is a case, not a KNOWN_GAPS entry. - import-target-index-reuse contract (#2909): membership-probe-only fixture (minimumScans: 0, same shape as Rust).
This commit is contained in:
parent
618133db98
commit
a69cb3a48a
5 changed files with 110 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ import { nodeToCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
|
|||
import { getZigParser, getZigScopeQuery } from './query.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
|
||||
export const ZIG_CONTAINER_TYPES: ReadonlySet<string> = new Set([
|
||||
'struct_declaration',
|
||||
|
|
@ -43,6 +44,51 @@ export function isZigContainerMethod(
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callable-value-flow vocabulary for tree-sitter-zig 1.1.2 (verified by AST
|
||||
* dump). Two grammar quirks drive the callbacks:
|
||||
*
|
||||
* - `variable_declaration` is FIELDLESS apart from `type:` — `const f = target;`
|
||||
* is `(variable_declaration (identifier) (identifier))`, and a bare
|
||||
* re-assignment statement `f = target;` parses to the SAME node shape. The
|
||||
* shared `left`/`name`/`value` fallback decomposes nothing, so
|
||||
* `extractAssignment` pairs first-identifier → last-child positionally.
|
||||
* `assignment_expression` (`self.f = target`) carries real `left`/`right`
|
||||
* fields and is left to the shared path.
|
||||
* - `call_expression` has NO argument-list wrapper: `invoke(second)` is
|
||||
* `(call_expression function: (identifier) (identifier))`. Arguments are
|
||||
* every named child other than the `function` field, hence
|
||||
* `extractCallArguments`.
|
||||
*
|
||||
* `builtin_function` (`@import`, `@sizeOf`, …) is deliberately not a call node:
|
||||
* builtins never take user callables as flow arguments.
|
||||
*/
|
||||
const ZIG_CALLABLE_CAPTURE_OPTIONS = {
|
||||
functionNodeTypes: new Set(['function_declaration']),
|
||||
callNodeTypes: new Set(['call_expression']),
|
||||
parameterListNodeTypes: new Set(['parameters']),
|
||||
parameterNodeTypes: new Set(['parameter']),
|
||||
bindingNodeTypes: new Set(['variable_declaration']),
|
||||
assignmentNodeTypes: new Set(['assignment_expression']),
|
||||
identifierNodeTypes: new Set(['identifier']),
|
||||
extractAssignment: (node: SyntaxNode) => {
|
||||
if (node.type !== 'variable_declaration') return undefined;
|
||||
const named = node.namedChildren.filter((child): child is SyntaxNode => child !== null);
|
||||
if (named.length < 2 || named[0]!.type !== 'identifier') return undefined;
|
||||
const source = named[named.length - 1]!;
|
||||
// `const x: T;` (extern) — the trailing child is the type, not a value.
|
||||
if (source.id === node.childForFieldName('type')?.id) return undefined;
|
||||
return { destination: named[0]!, source };
|
||||
},
|
||||
extractCallArguments: (call: SyntaxNode) => {
|
||||
const callee = call.childForFieldName('function');
|
||||
return call.namedChildren.filter(
|
||||
(child): child is SyntaxNode =>
|
||||
child !== null && child.id !== callee?.id && child.type !== 'comment',
|
||||
);
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function emitZigScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
|
|
@ -88,5 +134,7 @@ export function emitZigScopeCaptures(
|
|||
out.push(grouped);
|
||||
}
|
||||
|
||||
out.push(...synthesizeCallableFlowCaptures(tree.rootNode, ZIG_CALLABLE_CAPTURE_OPTIONS));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,15 @@ export interface CallableFlowCaptureOptions {
|
|||
| readonly { readonly destination: SyntaxNode; readonly source: SyntaxNode }[]
|
||||
| undefined;
|
||||
readonly extractFunctionParameters?: (node: SyntaxNode) => readonly SyntaxNode[] | undefined;
|
||||
/**
|
||||
* Provider-owned call-argument extraction, for grammars whose call node
|
||||
* carries its arguments as DIRECT children with no argument-list wrapper
|
||||
* (tree-sitter-zig's `call_expression`). Without a wrapper node the shared
|
||||
* `arguments`/`parameterListNodeTypes` lookup finds nothing, so every
|
||||
* `argument` fact is lost. Returning `undefined` falls back to the shared
|
||||
* path (mirrors `extractFunctionParameters`).
|
||||
*/
|
||||
readonly extractCallArguments?: (call: SyntaxNode) => readonly SyntaxNode[] | undefined;
|
||||
readonly extractCallCallee?: (node: SyntaxNode) => SyntaxNode | undefined;
|
||||
readonly isCallNode?: (node: SyntaxNode) => boolean;
|
||||
/**
|
||||
|
|
@ -891,6 +900,8 @@ function callArguments(
|
|||
call: SyntaxNode,
|
||||
options: CallableFlowCaptureOptions,
|
||||
): readonly SyntaxNode[] {
|
||||
const providerArguments = options.extractCallArguments?.(call);
|
||||
if (providerArguments !== undefined) return providerArguments;
|
||||
const list =
|
||||
call.childForFieldName('arguments') ??
|
||||
call.childForFieldName('argument') ??
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const CALLABLE_FLOW_PROVIDER_COVERAGE = {
|
|||
[SupportedLanguages.Dart]: 'matrix',
|
||||
[SupportedLanguages.Vue]: 'matrix',
|
||||
[SupportedLanguages.Cobol]: 'matrix',
|
||||
[SupportedLanguages.Zig]: 'matrix',
|
||||
} as const satisfies Record<SupportedLanguages, 'matrix' | 'dedicated'>;
|
||||
|
||||
const PROVIDER_FLOW_CASES = [
|
||||
|
|
@ -249,6 +250,21 @@ PROGRAM-ID. TARGET.
|
|||
PROCEDURE DIVISION.
|
||||
GOBACK.
|
||||
END PROGRAM TARGET.
|
||||
`,
|
||||
},
|
||||
{
|
||||
language: SupportedLanguages.Zig,
|
||||
extension: 'zig',
|
||||
caller: 'invoke',
|
||||
target: 'target',
|
||||
source: `
|
||||
fn target() void {}
|
||||
fn invoke(callback: *const fn () void) void { callback(); }
|
||||
pub fn main() void {
|
||||
const first = target;
|
||||
const second = first;
|
||||
invoke(second);
|
||||
}
|
||||
`,
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -362,6 +362,22 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|||
reachesDecoy: 'CUSTREC',
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.Zig,
|
||||
{
|
||||
// `@import("std")` is the standard library, and Zig's resolver answers
|
||||
// null for the stdlib names outright — it never suffix-matches a bare
|
||||
// name against the file set, so a repo file that happens to be called
|
||||
// `std.zig` is not a candidate. The same file IS reachable through the
|
||||
// filesystem-relative spelling, which is what the decoy arm proves.
|
||||
files: ['src/std.zig', 'src/util.zig', 'src/main.zig'],
|
||||
fromFile: 'src/main.zig',
|
||||
resolutionConfig: undefined,
|
||||
external: 'std',
|
||||
decoy: 'src/std.zig',
|
||||
reachesDecoy: 'std.zig',
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -528,6 +528,25 @@ const FIXTURES: ReadonlyMap<SupportedLanguages, ImportTargetFixture> = new Map<
|
|||
minimumParsedFileReads: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.Zig,
|
||||
{
|
||||
files: ['src/util.zig', 'src/main.zig'],
|
||||
fromFile: 'src/main.zig',
|
||||
resolutionConfig: undefined,
|
||||
// A relative `@import` that names no file: the resolver walks the path
|
||||
// arithmetically from `fromFile` and probes the Set with two `has` calls
|
||||
// (`<candidate>` and `<candidate>.zig`) — membership probes only, no
|
||||
// traversal, the same shape as Rust. Bare names go through the
|
||||
// build.zig.zon `.path` map (a Map lookup) and never touch the Set.
|
||||
missTarget: (i) => `ghost${i}.zig`,
|
||||
hitTarget: 'util.zig',
|
||||
parsedImport: IGNORES_CONTEXT,
|
||||
// See `minimumScans` on the interface: membership probes only.
|
||||
minimumScans: 0,
|
||||
minimumParsedFileReads: 0,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue