GitNexus/gitnexus/test/integration/resolvers/helpers.ts
Copilot e412d292fe
feat: migrate C to scope-based resolution (RFC #909 Ring 3) (#1481)
* Initial plan

* feat: add C scope resolution files for language migration (RFC #909)

Add 11 C language scope resolution files following the Go pattern:
- query.ts: tree-sitter-c query and parser for C constructs
- captures.ts: emit scope captures with arity enrichment
- import-decomposer.ts: decompose #include into structured captures
- arity-metadata.ts: C function declaration/call arity computation
- interpret.ts: interpret C imports and type bindings
- import-target.ts: resolve #include paths via suffix matching
- arity.ts: C arity compatibility (variadic detection)
- merge-bindings.ts: first-wins binding merge by tier
- simple-hooks.ts: null hooks (no receivers/methods in C)
- index.ts: barrel re-exports
- scope-resolver.ts: ScopeResolver implementation for C

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat: migrate C to scope-based resolution (RFC #909 Ring 3)

Add C ScopeResolver with:
- tree-sitter-c scope query (structs, unions, enums, functions, macros, variables, includes)
- emitCScopeCaptures with arity enrichment and typedef-struct dedup
- interpretCImport for #include directives (system headers filtered)
- resolveCImportTarget with suffix matching
- cArityCompatibility with variadic detection
- cMergeBindings (first-wins by tier)
- Header file scanning for cross-language #include resolution
- Register in SCOPE_RESOLVERS and MIGRATED_LANGUAGES
- Integration test with 4 passing test cases

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ddcbc075-2999-492c-a0ac-47cddd401a4b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: update registry-primary-flag test and add C legacy parity expected failures

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ddcbc075-2999-492c-a0ac-47cddd401a4b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: improve arity-metadata readability per review feedback

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ddcbc075-2999-492c-a0ac-47cddd401a4b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address CI failures — unused import, Dirent types, null comparison, lint, formatting

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f4b6e20d-8d56-4834-8296-db82af27f8e1

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: replace loose comparisons with strict equality, remove optional chaining from childForFieldName

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/20628629-9dc7-45ec-8ae4-f3a14ad29d93

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* Potential fix for pull request finding 'CodeQL / Comparison between inconvertible types'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix: remove unnecessary optional chaining on non-null decl in findFuncDeclarator

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9b012c35-7494-4180-8c6a-b83da8d8abb9

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address 5 production readiness review findings

Finding 1: Enforce static functions as file-local via expandsWildcardTo hook
- Add static-linkage.ts tracking module with markStaticName/isStaticName/expandCWildcardNames
- Update captures.ts to detect storage_class_specifier static on functions
- Wire expandsWildcardTo in scope-resolver.ts

Finding 2: Expand test coverage to ≥30 cases (74 unit tests added)
- c-captures.test.ts: 55 tests (scopes, structs, unions, enums, functions, typedef, field, variable, macro, imports, references, type bindings, arity, static)
- c-imports.test.ts: 12 tests (decomposition, interpretation, target resolution, determinism, edge cases)
- c-arity.test.ts: 18 tests (declaration arity, call arity, compatibility)

Finding 3: Deterministic #include resolution on depth ties
- Add lexicographic tiebreak in import-target.ts when candidates tie on path depth

Finding 4: Revert unexplained package-lock.json change
- Restored to pre-PR state (node >=20.0.0)

Finding 5: Planning artifact commit acknowledged (squash on merge)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/22ee780c-2b44-4e69-b9c4-8843ad6ec1ee

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address code review feedback — Set-based dedup, SyntaxNode type alias

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/22ee780c-2b44-4e69-b9c4-8843ad6ec1ee

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix: address second review findings 1-4 — isFileLocalDef hook, singleton docs, fn-ptr typedef, static isolation test

Finding 1: Added `isFileLocalDef` hook to ScopeResolver contract + implementation
in free-call-fallback.ts to filter C static functions from global free-call
fallback. Threads caller filePath through pickUniqueGlobalCallable so static
defs in other files are excluded.

Finding 2: Documented single-invocation assumption on staticNames Map. Added
clearStaticNames() call in loadResolutionConfig to prevent cross-repo
contamination in server-mode scenarios.

Finding 3: Added tree-sitter query pattern for function pointer typedef aliases
(typedef void (*callback)(int, int)) in query.ts. Added unit test.

Finding 4: Added c-static-isolation integration fixture (a.c with static helper,
b.c with non-static helper, caller.c) and test asserting no CALLS edge from
caller to a.c's static helper.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5b948327-9f59-4ca2-9d8c-8c8087feb510

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix: skip static isolation integration test in legacy parity mode

The `caller.c calls b:helper via include, NOT a:static helper` test
requires scope-based wildcard import binding + isFileLocalDef filtering
which is only available in the registry-primary path. The legacy DAG
path does not resolve cross-file calls through #include → prototype
chains. Added to LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3be48249-b375-4446-973b-657400f530fb

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address 3 review findings — static leakage in Phase 2, build-dir skip list, same-directory preference

Finding 1: Apply isFileLocalDef filtering in Phase 2 of pickUniqueGlobalCallable
so cross-file static defs cannot leak through the SemanticModel fallback path.

Finding 2: Expand scanHeaderFiles skip list with dist, build, out, target,
_build, .next, cmake-build-* to avoid generated headers shadowing source ones.

Finding 3: Implement same-directory sibling preference in resolveCImportTarget,
matching C compiler #include "…" relative-lookup semantics. Sibling check now
runs before exact match and suffix fallback.

Tests: 11 new header-scan tests, 4 new import-target tests (96 total C tests).

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5236f2b8-72a0-476d-bf39-cca041781014

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: skip redundant sibling path computation when targetRaw has no backslashes

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5236f2b8-72a0-476d-bf39-cca041781014

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix: normalize header-scan paths to forward slashes for Windows compatibility

On Windows, path.relative() produces backslash-separated paths (e.g. src\foo.h)
which caused test failures when comparing against forward-slash expectations.
Normalize output with .replace(/\\/g, '/') matching the convention used
throughout the scope-resolution pipeline.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/88420759-8a8c-4e6d-affa-ecb5458a235a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address 4 findings — K&R arity, function-pointer docs, prototype docs, _filePath rename

Finding 1: K&R empty parameter list `int foo()` now returns unknown arity `{}`
instead of `{parameterCount:0}`. Distinguishes from explicit `int foo(void)`.
3 unit tests cover K&R definition, prototype, and void comparison.

Finding 2: Added code comment documenting function-pointer-variable call
capture as known architectural trade-off (same as Go resolver).

Finding 3: Added code comment documenting prototype/definition duplication
as graph-quality concern (no false CALLS edges).

Finding 4: Renamed `_filePath` → `filePath` in captures.ts since it is
actively used in markStaticName().

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ab2164f7-972f-4ea8-81fa-a14ce20d7cce

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 09:02:00 +01:00

152 lines
5.9 KiB
TypeScript

/**
* Shared test helpers for language resolution integration tests.
*/
import path from 'path';
import { it as vitestIt } from 'vitest';
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineOptions } from '../../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../../src/types/pipeline.js';
import type { GraphRelationship } from 'gitnexus-shared';
const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, ReadonlySet<string>>> = {
c: new Set([
// The legacy DAG path does not resolve the main → create_service call
// because the function prototype in the .h file and the definition in
// the .c file create a dedup ambiguity. The registry-primary path
// resolves it via scope-based wildcard import binding.
'emits CALLS edges for cross-file function calls',
// The legacy DAG path does not resolve cross-file calls through
// #include → prototype chains. The scope-based path resolves
// caller.c → b.h → public_b via wildcard import binding +
// isFileLocalDef filtering of static functions.
'caller.c calls b:helper via include, NOT a:static helper',
]),
csharp: new Set([
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path',
// Generic type-argument USES edges are emitted by the registry-primary
// resolver only; the legacy DAG path does not synthesize these references.
'emits USES edges for generic type arguments',
]),
go: new Set([
// The legacy DAG path does not resolve method calls when the method is
// defined in a different file from the receiver type (go-split-method-owner
// fixture). This requires scope-based cross-file package-sibling resolution
// which is only available in the registry-primary path.
'resolves user.Save() to the method whose receiver type is declared in another package file',
]),
python: new Set([
// Suffix-fallback lex tiebreak depends on the registry-primary
// resolver's deterministic sort. The legacy resolver returns the
// first match in `Set` iteration order, which is insertion-order
// dependent and not aligned with this guarantee. Backporting the
// sort to legacy is out of scope.
'picks the lexicographically smaller path on equal-depth ties',
'binds the call to alpha/services/sync.py, not omega',
'lex tiebreak still picks alpha/services/sync.py with reversed file-write order',
]),
};
type ResolverParityEnv = Readonly<Record<string, string | undefined>>;
type VitestIt = typeof vitestIt;
type CallableIt = (name: string, ...args: unknown[]) => unknown;
export function resolverParityFlagName(languageSlug: string): string {
return `REGISTRY_PRIMARY_${languageSlug.toUpperCase().replace(/-/g, '_')}`;
}
export function isLegacyResolverParityRun(
languageSlug: string,
env: ResolverParityEnv = process.env,
): boolean {
const value = env[resolverParityFlagName(languageSlug)]?.trim().toLowerCase();
return value === '0' || value === 'false' || value === 'no';
}
export function isLegacyResolverParityExpectedFailure(
languageSlug: string,
testName: string,
env: ResolverParityEnv = process.env,
): boolean {
if (!isLegacyResolverParityRun(languageSlug, env)) return false;
return LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES[languageSlug]?.has(testName) ?? false;
}
export function createResolverParityIt(languageSlug: string): VitestIt {
const wrapped = ((name: string, ...args: unknown[]) => {
const runner = isLegacyResolverParityExpectedFailure(languageSlug, name)
? vitestIt.skip
: vitestIt;
return (runner as unknown as CallableIt)(name, ...args);
}) as VitestIt;
Object.assign(wrapped, vitestIt);
return wrapped;
}
export const FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');
export const CROSS_FILE_FIXTURES = path.resolve(
__dirname,
'..',
'..',
'fixtures',
'cross-file-binding',
);
export type RelEdge = {
source: string;
target: string;
sourceLabel: string;
targetLabel: string;
sourceFilePath: string;
targetFilePath: string;
rel: GraphRelationship;
};
export function getRelationships(result: PipelineResult, type: string): RelEdge[] {
const edges: RelEdge[] = [];
for (const rel of result.graph.iterRelationships()) {
if (rel.type === type) {
const sourceNode = result.graph.getNode(rel.sourceId);
const targetNode = result.graph.getNode(rel.targetId);
edges.push({
source: sourceNode?.properties.name ?? rel.sourceId,
target: targetNode?.properties.name ?? rel.targetId,
sourceLabel: sourceNode?.label ?? 'unknown',
targetLabel: targetNode?.label ?? 'unknown',
sourceFilePath: sourceNode?.properties.filePath ?? '',
targetFilePath: targetNode?.properties.filePath ?? '',
rel,
});
}
}
return edges;
}
export function getNodesByLabel(result: PipelineResult, label: string): string[] {
const names: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === label) names.push(n.properties.name);
});
return names.sort();
}
export function edgeSet(edges: Array<{ source: string; target: string }>): string[] {
return edges.map((e) => `${e.source}${e.target}`).sort();
}
/** Get graph nodes by label with full properties (for parameterTypes assertions). */
export function getNodesByLabelFull(
result: PipelineResult,
label: string,
): Array<{ name: string; properties: Record<string, any> }> {
const nodes: Array<{ name: string; properties: Record<string, any> }> = [];
result.graph.forEachNode((n) => {
if (n.label === label) nodes.push({ name: n.properties.name, properties: n.properties });
});
return nodes.sort((a, b) => a.name.localeCompare(b.name));
}
// Tests can pass { skipGraphPhases: true } as third arg for faster runs
// (skips MRO, community detection, and process extraction).
export { runPipelineFromRepo };
export type { PipelineOptions, PipelineResult };