gitnexus 1.6.5-rc.4 → 1.6.5-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
  2. package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
  3. package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
  4. package/dist/core/ingestion/languages/c/arity.js +18 -0
  5. package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
  6. package/dist/core/ingestion/languages/c/captures.js +105 -0
  7. package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
  8. package/dist/core/ingestion/languages/c/header-scan.js +55 -0
  9. package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
  10. package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
  11. package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
  12. package/dist/core/ingestion/languages/c/import-target.js +57 -0
  13. package/dist/core/ingestion/languages/c/index.d.ts +11 -0
  14. package/dist/core/ingestion/languages/c/index.js +11 -0
  15. package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
  16. package/dist/core/ingestion/languages/c/interpret.js +48 -0
  17. package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
  18. package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
  19. package/dist/core/ingestion/languages/c/query.d.ts +3 -0
  20. package/dist/core/ingestion/languages/c/query.js +161 -0
  21. package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
  22. package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
  23. package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
  24. package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
  25. package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
  26. package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
  27. package/dist/core/ingestion/languages/c-cpp.js +10 -0
  28. package/dist/core/ingestion/registry-primary-flag.js +1 -0
  29. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +11 -0
  30. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +2 -1
  31. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +13 -2
  32. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  33. package/dist/core/ingestion/scope-resolution/pipeline/run.js +4 -1
  34. package/dist/server/git-clone.js +10 -13
  35. package/dist/storage/git.d.ts +17 -6
  36. package/dist/storage/git.js +46 -12
  37. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
2
+ export interface CArityInfo {
3
+ parameterCount?: number;
4
+ requiredParameterCount?: number;
5
+ parameterTypes?: string[];
6
+ }
7
+ /**
8
+ * Compute declaration arity from a C function definition or declaration node.
9
+ */
10
+ export declare function computeCDeclarationArity(node: SyntaxNode): CArityInfo;
11
+ /**
12
+ * Compute call-site arity from a call_expression node.
13
+ */
14
+ export declare function computeCCallArity(node: SyntaxNode): number;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Compute declaration arity from a C function definition or declaration node.
3
+ */
4
+ export function computeCDeclarationArity(node) {
5
+ // Find the function_declarator child (may be wrapped in pointer_declarator)
6
+ const funcDecl = findFuncDeclarator(node);
7
+ if (funcDecl === null)
8
+ return {};
9
+ const paramList = funcDecl.childForFieldName('parameters');
10
+ if (paramList === null)
11
+ return {};
12
+ const params = [];
13
+ for (let i = 0; i < paramList.childCount; i++) {
14
+ const child = paramList.child(i);
15
+ if (child === null)
16
+ continue;
17
+ if (child.type === 'parameter_declaration' || child.type === 'variadic_parameter') {
18
+ params.push(child);
19
+ }
20
+ }
21
+ // K&R old-style declaration: `int foo()` has an empty parameter_list with
22
+ // no parameter_declaration or variadic_parameter children. Per C89/C99,
23
+ // this means the function accepts an unspecified number/types of arguments —
24
+ // NOT zero arguments. Return unknown arity to avoid false 'incompatible'.
25
+ // `int foo(void)` is the explicit zero-parameter form and is handled below.
26
+ if (params.length === 0)
27
+ return {};
28
+ // (void) means zero parameters
29
+ if (params.length === 1 && params[0].type === 'parameter_declaration') {
30
+ const typeNode = params[0].childForFieldName('type');
31
+ const hasDeclarator = params[0].childForFieldName('declarator') !== null;
32
+ if (typeNode !== null && typeNode.text === 'void' && !hasDeclarator) {
33
+ return { parameterCount: 0, requiredParameterCount: 0, parameterTypes: [] };
34
+ }
35
+ }
36
+ const isVariadic = params.some((p) => p.type === 'variadic_parameter');
37
+ const nonVariadicCount = params.filter((p) => p.type !== 'variadic_parameter').length;
38
+ const types = [];
39
+ for (const p of params) {
40
+ if (p.type === 'variadic_parameter') {
41
+ types.push('...');
42
+ }
43
+ else {
44
+ const typeNode = p.childForFieldName('type');
45
+ types.push(typeNode?.text ?? 'unknown');
46
+ }
47
+ }
48
+ return {
49
+ parameterCount: isVariadic ? undefined : nonVariadicCount,
50
+ requiredParameterCount: nonVariadicCount,
51
+ parameterTypes: types,
52
+ };
53
+ }
54
+ /**
55
+ * Compute call-site arity from a call_expression node.
56
+ */
57
+ export function computeCCallArity(node) {
58
+ const argList = node.childForFieldName('arguments');
59
+ if (argList === null)
60
+ return 0;
61
+ let count = 0;
62
+ for (let i = 0; i < argList.childCount; i++) {
63
+ const child = argList.child(i);
64
+ if (child === null)
65
+ continue;
66
+ // Skip punctuation (commas, parens)
67
+ if (child.type !== ',' && child.type !== '(' && child.type !== ')') {
68
+ count++;
69
+ }
70
+ }
71
+ return count;
72
+ }
73
+ function findFuncDeclarator(node) {
74
+ // Direct child
75
+ let decl = node.childForFieldName('declarator');
76
+ if (decl === null) {
77
+ for (let i = 0; i < node.childCount; i++) {
78
+ const c = node.child(i);
79
+ if (c?.type === 'function_declarator')
80
+ return c;
81
+ }
82
+ return null;
83
+ }
84
+ // Unwrap pointer_declarator
85
+ while (decl.type === 'pointer_declarator') {
86
+ const next = decl.childForFieldName('declarator');
87
+ if (next === null)
88
+ break;
89
+ decl = next;
90
+ }
91
+ if (decl.type === 'function_declarator')
92
+ return decl;
93
+ return null;
94
+ }
@@ -0,0 +1,6 @@
1
+ import type { Callsite, SymbolDefinition } from '../../../../_shared/index.js';
2
+ /**
3
+ * C arity compatibility: no overloading. Variadic functions detected
4
+ * via '...' in parameterTypes. Otherwise exact match or unknown.
5
+ */
6
+ export declare function cArityCompatibility(def: SymbolDefinition, callsite: Callsite): 'compatible' | 'unknown' | 'incompatible';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * C arity compatibility: no overloading. Variadic functions detected
3
+ * via '...' in parameterTypes. Otherwise exact match or unknown.
4
+ */
5
+ export function cArityCompatibility(def, callsite) {
6
+ const max = def.parameterCount;
7
+ const min = def.requiredParameterCount;
8
+ if (max === undefined && min === undefined)
9
+ return 'unknown';
10
+ if (!Number.isFinite(callsite.arity) || callsite.arity < 0)
11
+ return 'unknown';
12
+ const variadic = def.parameterTypes?.some((t) => t === '...') ?? false;
13
+ if (min !== undefined && callsite.arity < min)
14
+ return 'incompatible';
15
+ if (max !== undefined && callsite.arity > max && !variadic)
16
+ return 'incompatible';
17
+ return 'compatible';
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ export declare function emitCScopeCaptures(sourceText: string, filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
@@ -0,0 +1,105 @@
1
+ import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
2
+ import { getCParser, getCScopeQuery } from './query.js';
3
+ import { getTreeSitterBufferSize } from '../../constants.js';
4
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
5
+ import { splitCInclude } from './import-decomposer.js';
6
+ import { computeCDeclarationArity, computeCCallArity } from './arity-metadata.js';
7
+ import { markStaticName } from './static-linkage.js';
8
+ export function emitCScopeCaptures(sourceText, filePath, cachedTree) {
9
+ let tree = cachedTree;
10
+ if (tree === undefined) {
11
+ tree = parseSourceSafe(getCParser(), sourceText, undefined, {
12
+ bufferSize: getTreeSitterBufferSize(sourceText),
13
+ });
14
+ }
15
+ const rawMatches = getCScopeQuery().matches(tree.rootNode);
16
+ const out = [];
17
+ // Track ranges where typedef-struct/union was captured as @declaration.struct/union
18
+ // so we can suppress the duplicate @declaration.typedef match at the same range.
19
+ const structTypedefRanges = new Set();
20
+ for (const m of rawMatches) {
21
+ const grouped = {};
22
+ for (const c of m.captures) {
23
+ const tag = '@' + c.name;
24
+ if (tag.startsWith('@_'))
25
+ continue;
26
+ grouped[tag] = nodeToCapture(tag, c.node);
27
+ }
28
+ if (Object.keys(grouped).length === 0)
29
+ continue;
30
+ // Handle #include statements
31
+ if (grouped['@import.statement'] !== undefined) {
32
+ const anchor = grouped['@import.statement'];
33
+ const includeNode = findNodeAtRange(tree.rootNode, anchor.range, 'preproc_include');
34
+ if (includeNode !== null) {
35
+ const split = splitCInclude(includeNode);
36
+ if (split !== null) {
37
+ out.push(split);
38
+ continue;
39
+ }
40
+ }
41
+ }
42
+ // Track typedef-struct ranges to suppress duplicate typedef declarations
43
+ const structAnchor = grouped['@declaration.struct'] ?? grouped['@declaration.union'];
44
+ if (structAnchor !== undefined) {
45
+ const r = structAnchor.range;
46
+ structTypedefRanges.add(`${r.startLine}:${r.startCol}:${r.endLine}:${r.endCol}`);
47
+ }
48
+ // Suppress @declaration.typedef if the same range was already captured as struct/union
49
+ const typedefAnchor = grouped['@declaration.typedef'];
50
+ if (typedefAnchor !== undefined) {
51
+ const r = typedefAnchor.range;
52
+ const key = `${r.startLine}:${r.startCol}:${r.endLine}:${r.endCol}`;
53
+ if (structTypedefRanges.has(key))
54
+ continue;
55
+ }
56
+ // Enrich function declarations with arity metadata and detect static linkage
57
+ const declAnchor = grouped['@declaration.function'];
58
+ if (declAnchor !== undefined) {
59
+ const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_definition') ??
60
+ findNodeAtRange(tree.rootNode, declAnchor.range, 'declaration');
61
+ if (fnNode !== null) {
62
+ const arity = computeCDeclarationArity(fnNode);
63
+ if (arity.parameterCount !== undefined) {
64
+ grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
65
+ }
66
+ if (arity.requiredParameterCount !== undefined) {
67
+ grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
68
+ }
69
+ if (arity.parameterTypes !== undefined) {
70
+ grouped['@declaration.parameter-types'] = syntheticCapture('@declaration.parameter-types', fnNode, JSON.stringify(arity.parameterTypes));
71
+ }
72
+ // Detect static storage class (file-local linkage)
73
+ if (hasStaticStorageClass(fnNode)) {
74
+ const nameText = grouped['@declaration.name']?.text;
75
+ if (nameText !== undefined) {
76
+ markStaticName(filePath, nameText);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ // Enrich call references with arity
82
+ const callAnchor = grouped['@reference.call.free'] ?? grouped['@reference.call.member'];
83
+ if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
84
+ const callNode = findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression');
85
+ if (callNode !== null) {
86
+ grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(computeCCallArity(callNode)));
87
+ }
88
+ }
89
+ out.push(grouped);
90
+ }
91
+ return out;
92
+ }
93
+ /**
94
+ * Check if a C function_definition or declaration has `static` storage class.
95
+ * Walks direct children for a `storage_class_specifier` node with text `static`.
96
+ */
97
+ function hasStaticStorageClass(node) {
98
+ for (let i = 0; i < node.childCount; i++) {
99
+ const child = node.child(i);
100
+ if (child !== null && child.type === 'storage_class_specifier' && child.text === 'static') {
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Walk `repoPath` recursively and return relative paths of all `.h` files.
3
+ * Used by `loadResolutionConfig` so the C resolver can resolve `#include`
4
+ * targets that live in `.h` files (classified as C++ by language detection
5
+ * but importable from `.c` files).
6
+ */
7
+ export declare function scanHeaderFiles(repoPath: string): ReadonlySet<string>;
@@ -0,0 +1,55 @@
1
+ import { readdirSync } from 'fs';
2
+ import { join, relative } from 'path';
3
+ /** C header extensions to scan for in the workspace. */
4
+ const HEADER_EXTENSIONS = new Set(['.h']);
5
+ /**
6
+ * Walk `repoPath` recursively and return relative paths of all `.h` files.
7
+ * Used by `loadResolutionConfig` so the C resolver can resolve `#include`
8
+ * targets that live in `.h` files (classified as C++ by language detection
9
+ * but importable from `.c` files).
10
+ */
11
+ export function scanHeaderFiles(repoPath) {
12
+ const headers = new Set();
13
+ walk(repoPath, repoPath, headers);
14
+ return headers;
15
+ }
16
+ function walk(dir, root, out) {
17
+ let entries;
18
+ try {
19
+ entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' });
20
+ }
21
+ catch {
22
+ return; // permission denied, etc.
23
+ }
24
+ for (const entry of entries) {
25
+ const name = entry.name;
26
+ const full = join(dir, name);
27
+ if (entry.isDirectory()) {
28
+ // Skip common non-source directories and build output dirs.
29
+ // Build dirs (dist, build, out, target, _build, .next, cmake-build-*)
30
+ // may contain generated headers that shadow source headers.
31
+ if (name === 'node_modules' ||
32
+ name === '.git' ||
33
+ name === 'vendor' ||
34
+ name === 'dist' ||
35
+ name === 'build' ||
36
+ name === 'out' ||
37
+ name === 'target' ||
38
+ name === '_build' ||
39
+ name === '.next' ||
40
+ name.startsWith('cmake-build')) {
41
+ continue;
42
+ }
43
+ walk(full, root, out);
44
+ }
45
+ else if (entry.isFile()) {
46
+ const ext = name.slice(name.lastIndexOf('.'));
47
+ if (HEADER_EXTENSIONS.has(ext)) {
48
+ // Normalize to forward slashes for cross-platform consistency.
49
+ // path.relative() returns backslash-separated paths on Windows,
50
+ // but the scope-resolution pipeline uses forward slashes uniformly.
51
+ out.add(relative(root, full).replace(/\\/g, '/'));
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,8 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ import { type SyntaxNode } from '../../utils/ast-helpers.js';
3
+ /**
4
+ * Decompose a `preproc_include` node into a CaptureMatch with structured
5
+ * import captures. C #include maps to a wildcard import (all symbols
6
+ * from the header are visible).
7
+ */
8
+ export declare function splitCInclude(node: SyntaxNode): CaptureMatch | null;
@@ -0,0 +1,50 @@
1
+ import { nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
2
+ /**
3
+ * Decompose a `preproc_include` node into a CaptureMatch with structured
4
+ * import captures. C #include maps to a wildcard import (all symbols
5
+ * from the header are visible).
6
+ */
7
+ export function splitCInclude(node) {
8
+ // node.type === 'preproc_include'
9
+ // path field: (string_literal (string_content)) | (system_lib_string)
10
+ const pathNode = node.childForFieldName?.('path') ?? null;
11
+ if (pathNode === null) {
12
+ // Fallback: scan children
13
+ for (let i = 0; i < node.childCount; i++) {
14
+ const child = node.child(i);
15
+ if (child === null)
16
+ continue;
17
+ if (child.type === 'string_literal' || child.type === 'system_lib_string') {
18
+ return buildIncludeCapture(node, child);
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ return buildIncludeCapture(node, pathNode);
24
+ }
25
+ function buildIncludeCapture(node, pathNode) {
26
+ let raw;
27
+ if (pathNode.type === 'string_literal') {
28
+ // string_literal has children: `"`, string_content, `"`
29
+ // Use namedChildren to find the string_content node
30
+ const content = pathNode.namedChildren.find((c) => c.type === 'string_content');
31
+ raw = content?.text ?? pathNode.text.replace(/^"|"$/g, '');
32
+ }
33
+ else {
34
+ // system_lib_string: <stdio.h> → strip angle brackets
35
+ raw = pathNode.text;
36
+ if (raw.startsWith('<') && raw.endsWith('>')) {
37
+ raw = raw.slice(1, -1);
38
+ }
39
+ }
40
+ const isSystem = pathNode.type === 'system_lib_string';
41
+ const result = {
42
+ '@import.statement': nodeToCapture('@import.statement', node),
43
+ '@import.kind': syntheticCapture('@import.kind', node, 'wildcard'),
44
+ '@import.source': syntheticCapture('@import.source', node, raw),
45
+ };
46
+ if (isSystem) {
47
+ result['@import.system'] = syntheticCapture('@import.system', node, 'true');
48
+ }
49
+ return result;
50
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve a C #include path to a file in the workspace.
3
+ *
4
+ * Strategy:
5
+ * 1. Check for a same-directory sibling relative to the including file
6
+ * (matches C compiler `#include "…"` relative-lookup semantics).
7
+ * 2. Check for an exact match (path as-is in the workspace).
8
+ * 3. Fall back to suffix matching against all workspace file paths.
9
+ * Tie-breaking: prefer the match with the fewest path components
10
+ * (closest to root). On equal depth, break ties lexicographically
11
+ * by normalized path to ensure deterministic resolution regardless
12
+ * of filesystem iteration order.
13
+ */
14
+ export declare function resolveCImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>): string | null;
@@ -0,0 +1,57 @@
1
+ import { dirname, join } from 'path';
2
+ /**
3
+ * Resolve a C #include path to a file in the workspace.
4
+ *
5
+ * Strategy:
6
+ * 1. Check for a same-directory sibling relative to the including file
7
+ * (matches C compiler `#include "…"` relative-lookup semantics).
8
+ * 2. Check for an exact match (path as-is in the workspace).
9
+ * 3. Fall back to suffix matching against all workspace file paths.
10
+ * Tie-breaking: prefer the match with the fewest path components
11
+ * (closest to root). On equal depth, break ties lexicographically
12
+ * by normalized path to ensure deterministic resolution regardless
13
+ * of filesystem iteration order.
14
+ */
15
+ export function resolveCImportTarget(targetRaw, fromFile, allFilePaths) {
16
+ if (!targetRaw)
17
+ return null;
18
+ const normalizedTarget = targetRaw.replace(/\\/g, '/');
19
+ // Same-directory sibling first: mirrors the C compiler's #include "…"
20
+ // relative-lookup semantics where the directory of the including
21
+ // file is searched before the include-path list.
22
+ if (fromFile) {
23
+ const siblingRaw = join(dirname(fromFile), targetRaw);
24
+ const sibling = siblingRaw.replace(/\\/g, '/');
25
+ if (allFilePaths.has(sibling))
26
+ return sibling;
27
+ // When targetRaw contains backslashes, the normalized form may
28
+ // resolve to a different sibling path — try it as well.
29
+ if (targetRaw !== normalizedTarget) {
30
+ const siblingAlt = join(dirname(fromFile), normalizedTarget);
31
+ const siblingAltNorm = siblingAlt.replace(/\\/g, '/');
32
+ if (allFilePaths.has(siblingAltNorm))
33
+ return siblingAltNorm;
34
+ }
35
+ }
36
+ // Exact match (path as-is in the workspace)
37
+ if (allFilePaths.has(normalizedTarget))
38
+ return normalizedTarget;
39
+ // Suffix match: find files ending with /targetRaw or equal to targetRaw
40
+ const suffix = '/' + normalizedTarget;
41
+ let bestMatch = null;
42
+ let bestDepth = Infinity;
43
+ let bestNormalized = '';
44
+ for (const filePath of allFilePaths) {
45
+ const normalized = filePath.replace(/\\/g, '/');
46
+ if (normalized === normalizedTarget || normalized.endsWith(suffix)) {
47
+ // Prefer shortest path (closest match)
48
+ const depth = normalized.split('/').length;
49
+ if (depth < bestDepth || (depth === bestDepth && normalized < bestNormalized)) {
50
+ bestDepth = depth;
51
+ bestMatch = filePath;
52
+ bestNormalized = normalized;
53
+ }
54
+ }
55
+ }
56
+ return bestMatch;
57
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * C scope-resolution hooks (RFC #909 Ring 3).
3
+ */
4
+ export { emitCScopeCaptures } from './captures.js';
5
+ export { interpretCImport, interpretCTypeBinding, normalizeCTypeName } from './interpret.js';
6
+ export { splitCInclude } from './import-decomposer.js';
7
+ export { cArityCompatibility } from './arity.js';
8
+ export { cMergeBindings } from './merge-bindings.js';
9
+ export { cBindingScopeFor, cImportOwningScope, cReceiverBinding } from './simple-hooks.js';
10
+ export { resolveCImportTarget } from './import-target.js';
11
+ export { markStaticName, isStaticName, clearStaticNames, expandCWildcardNames, } from './static-linkage.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * C scope-resolution hooks (RFC #909 Ring 3).
3
+ */
4
+ export { emitCScopeCaptures } from './captures.js';
5
+ export { interpretCImport, interpretCTypeBinding, normalizeCTypeName } from './interpret.js';
6
+ export { splitCInclude } from './import-decomposer.js';
7
+ export { cArityCompatibility } from './arity.js';
8
+ export { cMergeBindings } from './merge-bindings.js';
9
+ export { cBindingScopeFor, cImportOwningScope, cReceiverBinding } from './simple-hooks.js';
10
+ export { resolveCImportTarget } from './import-target.js';
11
+ export { markStaticName, isStaticName, clearStaticNames, expandCWildcardNames, } from './static-linkage.js';
@@ -0,0 +1,14 @@
1
+ import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from '../../../../_shared/index.js';
2
+ /**
3
+ * Interpret a C #include capture into a ParsedImport.
4
+ * C includes are always wildcard imports (all symbols from the header).
5
+ */
6
+ export declare function interpretCImport(captures: CaptureMatch): ParsedImport | null;
7
+ /**
8
+ * Interpret a C type-binding capture into a ParsedTypeBinding.
9
+ */
10
+ export declare function interpretCTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null;
11
+ /**
12
+ * Normalize a C type name: strip pointer/array syntax, qualifiers.
13
+ */
14
+ export declare function normalizeCTypeName(text: string): string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Interpret a C #include capture into a ParsedImport.
3
+ * C includes are always wildcard imports (all symbols from the header).
4
+ */
5
+ export function interpretCImport(captures) {
6
+ const source = captures['@import.source']?.text;
7
+ if (source === undefined)
8
+ return null;
9
+ // System headers (e.g. <stdio.h>) are not resolved to local files
10
+ if (captures['@import.system'] !== undefined)
11
+ return null;
12
+ return { kind: 'wildcard', targetRaw: source };
13
+ }
14
+ /**
15
+ * Interpret a C type-binding capture into a ParsedTypeBinding.
16
+ */
17
+ export function interpretCTypeBinding(captures) {
18
+ const name = captures['@type-binding.name']?.text;
19
+ const type = captures['@type-binding.type']?.text;
20
+ if (name === undefined || type === undefined)
21
+ return null;
22
+ let source = 'annotation';
23
+ if (captures['@type-binding.parameter'] !== undefined) {
24
+ source = 'parameter-annotation';
25
+ }
26
+ else if (captures['@type-binding.assignment'] !== undefined) {
27
+ source = 'assignment-inferred';
28
+ }
29
+ return { boundName: name, rawTypeName: normalizeCTypeName(type), source };
30
+ }
31
+ /**
32
+ * Normalize a C type name: strip pointer/array syntax, qualifiers.
33
+ */
34
+ export function normalizeCTypeName(text) {
35
+ let t = text.trim();
36
+ // Strip const, volatile, restrict qualifiers
37
+ t = t.replace(/\b(const|volatile|restrict|static|extern|inline)\b/g, '').trim();
38
+ // Strip pointer stars
39
+ while (t.endsWith('*'))
40
+ t = t.slice(0, -1).trim();
41
+ while (t.startsWith('*'))
42
+ t = t.slice(1).trim();
43
+ // Strip array brackets
44
+ t = t.replace(/\[.*?\]/g, '').trim();
45
+ // Strip struct/union/enum prefixes
46
+ t = t.replace(/^(struct|union|enum)\s+/, '');
47
+ return t;
48
+ }
@@ -0,0 +1,7 @@
1
+ import type { BindingRef } from '../../../../_shared/index.js';
2
+ /**
3
+ * C merge bindings: simple first-wins by tier (local > import > wildcard).
4
+ * C has no namespaces or reexports, but the tiers are defined for
5
+ * compatibility with the shared infrastructure.
6
+ */
7
+ export declare function cMergeBindings(existing: readonly BindingRef[], incoming: readonly BindingRef[], _scopeId: string): BindingRef[];
@@ -0,0 +1,23 @@
1
+ const TIER = {
2
+ local: 0,
3
+ namespace: 1,
4
+ import: 2,
5
+ reexport: 3,
6
+ wildcard: 4,
7
+ };
8
+ /**
9
+ * C merge bindings: simple first-wins by tier (local > import > wildcard).
10
+ * C has no namespaces or reexports, but the tiers are defined for
11
+ * compatibility with the shared infrastructure.
12
+ */
13
+ export function cMergeBindings(existing, incoming, _scopeId) {
14
+ const seen = new Set();
15
+ return [...existing, ...incoming]
16
+ .sort((a, b) => (TIER[a.origin] ?? 99) - (TIER[b.origin] ?? 99) || a.def.nodeId.localeCompare(b.def.nodeId))
17
+ .filter((binding) => {
18
+ if (seen.has(binding.def.nodeId))
19
+ return false;
20
+ seen.add(binding.def.nodeId);
21
+ return true;
22
+ });
23
+ }
@@ -0,0 +1,3 @@
1
+ import Parser from 'tree-sitter';
2
+ export declare function getCParser(): Parser;
3
+ export declare function getCScopeQuery(): Parser.Query;
@@ -0,0 +1,161 @@
1
+ import Parser from 'tree-sitter';
2
+ import C from 'tree-sitter-c';
3
+ const C_SCOPE_QUERY = `
4
+ ;; Scopes
5
+ (translation_unit) @scope.module
6
+ (struct_specifier) @scope.class
7
+ (union_specifier) @scope.class
8
+ (function_definition) @scope.function
9
+ (compound_statement) @scope.block
10
+ (if_statement) @scope.block
11
+ (for_statement) @scope.block
12
+ (while_statement) @scope.block
13
+ (do_statement) @scope.block
14
+ (switch_statement) @scope.block
15
+ (case_statement) @scope.block
16
+
17
+ ;; Declarations — struct (named)
18
+ (struct_specifier
19
+ name: (type_identifier) @declaration.name
20
+ body: (field_declaration_list)) @declaration.struct
21
+
22
+ ;; Declarations — struct (typedef struct { ... } Name)
23
+ (type_definition
24
+ type: (struct_specifier
25
+ body: (field_declaration_list))
26
+ declarator: (type_identifier) @declaration.name) @declaration.struct
27
+
28
+ ;; Declarations — union (named)
29
+ (union_specifier
30
+ name: (type_identifier) @declaration.name
31
+ body: (field_declaration_list)) @declaration.union
32
+
33
+ ;; Declarations — union (typedef union { ... } Name)
34
+ (type_definition
35
+ type: (union_specifier
36
+ body: (field_declaration_list))
37
+ declarator: (type_identifier) @declaration.name) @declaration.union
38
+
39
+ ;; Declarations — enum
40
+ (enum_specifier
41
+ name: (type_identifier) @declaration.name) @declaration.enum
42
+
43
+ ;; Declarations — function definition
44
+ (function_definition
45
+ declarator: (function_declarator
46
+ declarator: (identifier) @declaration.name)) @declaration.function
47
+
48
+ ;; Declarations — function definition with pointer return
49
+ (function_definition
50
+ declarator: (pointer_declarator
51
+ declarator: (function_declarator
52
+ declarator: (identifier) @declaration.name))) @declaration.function
53
+
54
+ ;; Declarations — function declaration (prototype)
55
+ ;; Note: Both prototypes and definitions are captured as @declaration.function.
56
+ ;; This may produce duplicate Function nodes in the knowledge graph when a
57
+ ;; function is declared in a header and defined in a .c file. CALLS edges
58
+ ;; resolve correctly through scope-based wildcard import chains; the
59
+ ;; duplication is a graph-quality concern only (no false edges).
60
+ (declaration
61
+ declarator: (function_declarator
62
+ declarator: (identifier) @declaration.name)) @declaration.function
63
+
64
+ ;; Declarations — function declaration with pointer return (prototype)
65
+ (declaration
66
+ declarator: (pointer_declarator
67
+ declarator: (function_declarator
68
+ declarator: (identifier) @declaration.name))) @declaration.function
69
+
70
+ ;; Declarations — typedef
71
+ (type_definition
72
+ declarator: (type_identifier) @declaration.name) @declaration.typedef
73
+
74
+ ;; Declarations — typedef for function pointers: typedef void (*callback)(int, int)
75
+ (type_definition
76
+ declarator: (function_declarator
77
+ declarator: (parenthesized_declarator
78
+ (pointer_declarator
79
+ declarator: (type_identifier) @declaration.name)))) @declaration.typedef
80
+
81
+ ;; Declarations — struct fields
82
+ (field_declaration
83
+ declarator: (field_identifier) @declaration.name) @declaration.field
84
+
85
+ ;; Declarations — struct fields (pointer)
86
+ (field_declaration
87
+ declarator: (pointer_declarator
88
+ declarator: (field_identifier) @declaration.name)) @declaration.field
89
+
90
+ ;; Declarations — variables (with initializer)
91
+ (declaration
92
+ declarator: (init_declarator
93
+ declarator: (identifier) @declaration.name)) @declaration.variable
94
+
95
+ ;; Declarations — macro definitions
96
+ (preproc_def
97
+ name: (identifier) @declaration.name) @declaration.macro
98
+
99
+ (preproc_function_def
100
+ name: (identifier) @declaration.name) @declaration.macro
101
+
102
+ ;; Declarations — enum constants
103
+ (enumerator
104
+ name: (identifier) @declaration.name) @declaration.const
105
+
106
+ ;; Imports
107
+ (preproc_include) @import.statement
108
+
109
+ ;; Type bindings — parameter annotations
110
+ (parameter_declaration
111
+ type: (_) @type-binding.type
112
+ declarator: (identifier) @type-binding.name) @type-binding.parameter
113
+
114
+ ;; Type bindings — variable with type (init_declarator)
115
+ (declaration
116
+ type: (_) @type-binding.type
117
+ declarator: (init_declarator
118
+ declarator: (identifier) @type-binding.name)) @type-binding.assignment
119
+
120
+ ;; References — free calls
121
+ ;; Note: This also captures calls through function pointer variables (e.g. fp(x))
122
+ ;; since tree-sitter-c produces structurally identical AST nodes for both direct
123
+ ;; function calls and function-pointer-variable calls. A type-based guard to
124
+ ;; distinguish variable-calls from function-calls is not implemented — this is a
125
+ ;; known architectural trade-off shared with the Go resolver. The uniqueness
126
+ ;; constraint in pickUniqueGlobalCallable limits false edge exposure.
127
+ (call_expression
128
+ function: (identifier) @reference.name) @reference.call.free
129
+
130
+ ;; References — member calls via pointer (ptr->func())
131
+ (call_expression
132
+ function: (field_expression
133
+ argument: (_) @reference.receiver
134
+ field: (field_identifier) @reference.name)) @reference.call.member
135
+
136
+ ;; References — field reads
137
+ (field_expression
138
+ argument: (_) @reference.receiver
139
+ field: (field_identifier) @reference.name) @reference.read
140
+
141
+ ;; References — field writes (assignment)
142
+ (assignment_expression
143
+ left: (field_expression
144
+ argument: (_) @reference.receiver
145
+ field: (field_identifier) @reference.name)) @reference.write
146
+ `;
147
+ let _parser = null;
148
+ let _query = null;
149
+ export function getCParser() {
150
+ if (_parser === null) {
151
+ _parser = new Parser();
152
+ _parser.setLanguage(C);
153
+ }
154
+ return _parser;
155
+ }
156
+ export function getCScopeQuery() {
157
+ if (_query === null) {
158
+ _query = new Parser.Query(C, C_SCOPE_QUERY);
159
+ }
160
+ return _query;
161
+ }
@@ -0,0 +1,13 @@
1
+ import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
2
+ /**
3
+ * C `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
4
+ * the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
5
+ *
6
+ * C is a structurally simple language for scope resolution:
7
+ * - No classes (structs are value types, no method dispatch)
8
+ * - No inheritance (no MRO needed beyond the shared first-wins default)
9
+ * - No overloading (arity check is simple: variadic detection only)
10
+ * - `#include` is wildcard import (all symbols from header are visible)
11
+ * - `static` functions are file-local (not exported)
12
+ */
13
+ export declare const cScopeResolver: ScopeResolver;
@@ -0,0 +1,60 @@
1
+ import { SupportedLanguages } from '../../../../_shared/index.js';
2
+ import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
3
+ import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
4
+ import { cProvider } from '../c-cpp.js';
5
+ import { cArityCompatibility, cMergeBindings, resolveCImportTarget } from './index.js';
6
+ import { scanHeaderFiles } from './header-scan.js';
7
+ import { expandCWildcardNames, isStaticName, clearStaticNames } from './static-linkage.js';
8
+ /**
9
+ * C `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
10
+ * the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
11
+ *
12
+ * C is a structurally simple language for scope resolution:
13
+ * - No classes (structs are value types, no method dispatch)
14
+ * - No inheritance (no MRO needed beyond the shared first-wins default)
15
+ * - No overloading (arity check is simple: variadic detection only)
16
+ * - `#include` is wildcard import (all symbols from header are visible)
17
+ * - `static` functions are file-local (not exported)
18
+ */
19
+ export const cScopeResolver = {
20
+ language: SupportedLanguages.C,
21
+ languageProvider: cProvider,
22
+ importEdgeReason: 'c-scope: include',
23
+ loadResolutionConfig: (repoPath) => {
24
+ // Clear stale static-linkage data from any previous invocation to
25
+ // prevent cross-repo contamination in server-mode scenarios.
26
+ clearStaticNames();
27
+ return scanHeaderFiles(repoPath);
28
+ },
29
+ resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
30
+ // Augment allFilePaths with .h files discovered via loadResolutionConfig
31
+ // since the phase only passes .c files to the C resolver but #include
32
+ // targets .h files classified as C++ in language detection.
33
+ const headerPaths = resolutionConfig;
34
+ if (headerPaths !== undefined && headerPaths.size > 0) {
35
+ const augmented = new Set(allFilePaths);
36
+ for (const h of headerPaths)
37
+ augmented.add(h);
38
+ return resolveCImportTarget(targetRaw, fromFile, augmented);
39
+ }
40
+ return resolveCImportTarget(targetRaw, fromFile, allFilePaths);
41
+ },
42
+ expandsWildcardTo: (targetModuleScope, parsedFiles) => expandCWildcardNames(targetModuleScope, parsedFiles),
43
+ mergeBindings: (existing, incoming, scopeId) => cMergeBindings(existing, incoming, scopeId),
44
+ arityCompatibility: (callsite, def) => cArityCompatibility(def, callsite),
45
+ buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
46
+ populateOwners: (parsed) => populateClassOwnedMembers(parsed),
47
+ isSuperReceiver: () => false,
48
+ // C is statically typed — disable field fallback heuristic
49
+ fieldFallbackOnMethodLookup: false,
50
+ // C has no method return types to propagate
51
+ propagatesReturnTypesAcrossImports: false,
52
+ // C #include brings in all symbols — enable global free call fallback
53
+ allowGlobalFreeCallFallback: true,
54
+ // C `static` functions have file-local (translation-unit) linkage —
55
+ // exclude them from global free-call fallback cross-file resolution.
56
+ isFileLocalDef: (def) => {
57
+ const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
58
+ return isStaticName(def.filePath, simple);
59
+ },
60
+ };
@@ -0,0 +1,14 @@
1
+ import type { CaptureMatch, ParsedImport, Scope, ScopeId, ScopeTree, TypeRef } from '../../../../_shared/index.js';
2
+ /**
3
+ * C binding scope: always use default auto-hoist (null).
4
+ * C has no self/receiver bindings that need special scoping.
5
+ */
6
+ export declare function cBindingScopeFor(_decl: CaptureMatch, _innermost: Scope, _tree: ScopeTree): ScopeId | null;
7
+ /**
8
+ * C import owning scope: always use default (null).
9
+ */
10
+ export declare function cImportOwningScope(_imp: ParsedImport, _innermost: Scope, _tree: ScopeTree): ScopeId | null;
11
+ /**
12
+ * C receiver binding: always null. C has no methods or receivers.
13
+ */
14
+ export declare function cReceiverBinding(_functionScope: Scope): TypeRef | null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * C binding scope: always use default auto-hoist (null).
3
+ * C has no self/receiver bindings that need special scoping.
4
+ */
5
+ export function cBindingScopeFor(_decl, _innermost, _tree) {
6
+ return null;
7
+ }
8
+ /**
9
+ * C import owning scope: always use default (null).
10
+ */
11
+ export function cImportOwningScope(_imp, _innermost, _tree) {
12
+ return null;
13
+ }
14
+ /**
15
+ * C receiver binding: always null. C has no methods or receivers.
16
+ */
17
+ export function cReceiverBinding(_functionScope) {
18
+ return null;
19
+ }
@@ -0,0 +1,13 @@
1
+ import type { ParsedFile, ScopeId } from '../../../../_shared/index.js';
2
+ /** Record a symbol name as `static` (file-local linkage) for the given file. */
3
+ export declare function markStaticName(filePath: string, name: string): void;
4
+ /** Check whether a symbol name has `static` linkage in the given file. */
5
+ export declare function isStaticName(filePath: string, name: string): boolean;
6
+ /** Clear tracked static names (for testing). */
7
+ export declare function clearStaticNames(): void;
8
+ /**
9
+ * Return the names visible through a C wildcard import (`#include`).
10
+ * All module-scope defs from the target file are visible EXCEPT those
11
+ * declared with `static` storage class (file-local linkage in C).
12
+ */
13
+ export declare function expandCWildcardNames(targetModuleScope: ScopeId, parsedFiles: readonly ParsedFile[]): readonly string[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Per-file set of function names declared with `static` storage class.
3
+ * Populated during `emitCScopeCaptures` and consumed by `expandCWildcardNames`
4
+ * to exclude file-local symbols from cross-file wildcard import visibility.
5
+ *
6
+ * NOTE: module-level state, single-process-single-repo use only.
7
+ * For server-mode or multi-repo-in-one-process use cases, call
8
+ * `clearStaticNames()` at the start of each resolution pass to avoid
9
+ * stale static-linkage data from a previous invocation.
10
+ *
11
+ * Key: filePath, Value: Set of static function names.
12
+ */
13
+ const staticNames = new Map();
14
+ /** Record a symbol name as `static` (file-local linkage) for the given file. */
15
+ export function markStaticName(filePath, name) {
16
+ let names = staticNames.get(filePath);
17
+ if (names === undefined) {
18
+ names = new Set();
19
+ staticNames.set(filePath, names);
20
+ }
21
+ names.add(name);
22
+ }
23
+ /** Check whether a symbol name has `static` linkage in the given file. */
24
+ export function isStaticName(filePath, name) {
25
+ return staticNames.get(filePath)?.has(name) ?? false;
26
+ }
27
+ /** Clear tracked static names (for testing). */
28
+ export function clearStaticNames() {
29
+ staticNames.clear();
30
+ }
31
+ /**
32
+ * Return the names visible through a C wildcard import (`#include`).
33
+ * All module-scope defs from the target file are visible EXCEPT those
34
+ * declared with `static` storage class (file-local linkage in C).
35
+ */
36
+ export function expandCWildcardNames(targetModuleScope, parsedFiles) {
37
+ const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
38
+ if (target === undefined)
39
+ return [];
40
+ const seen = new Set();
41
+ const names = [];
42
+ for (const def of target.localDefs) {
43
+ const name = simpleName(def);
44
+ if (name === '')
45
+ continue;
46
+ if (isStaticName(target.filePath, name))
47
+ continue;
48
+ if (seen.has(name))
49
+ continue;
50
+ seen.add(name);
51
+ names.push(name);
52
+ }
53
+ return names;
54
+ }
55
+ function simpleName(def) {
56
+ return def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
57
+ }
@@ -37,6 +37,7 @@ import { createCallExtractor } from '../call-extractors/generic.js';
37
37
  import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js';
38
38
  import { createHeritageExtractor } from '../heritage-extractors/generic.js';
39
39
  import { stripUeMacros } from '../cpp-ue-preprocessor.js';
40
+ import { emitCScopeCaptures, interpretCImport, interpretCTypeBinding, cArityCompatibility, cBindingScopeFor, cImportOwningScope, cReceiverBinding, } from './c/index.js';
40
41
  const C_BUILT_INS = new Set([
41
42
  'printf',
42
43
  'fprintf',
@@ -338,6 +339,15 @@ export const cProvider = defineLanguage({
338
339
  heritageExtractor: createHeritageExtractor(SupportedLanguages.C),
339
340
  labelOverride: cppLabelOverride,
340
341
  builtInNames: C_BUILT_INS,
342
+ // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
343
+ emitScopeCaptures: emitCScopeCaptures,
344
+ interpretImport: interpretCImport,
345
+ interpretTypeBinding: interpretCTypeBinding,
346
+ bindingScopeFor: cBindingScopeFor,
347
+ importOwningScope: cImportOwningScope,
348
+ receiverBinding: cReceiverBinding,
349
+ arityCompatibility: cArityCompatibility,
350
+ // mergeBindings + resolveImportTarget live on ScopeResolver (see c/scope-resolver.ts).
341
351
  });
342
352
  export const cppProvider = defineLanguage({
343
353
  id: SupportedLanguages.CPlusPlus,
@@ -69,6 +69,7 @@ export const MIGRATED_LANGUAGES = new Set([
69
69
  SupportedLanguages.CSharp,
70
70
  SupportedLanguages.TypeScript,
71
71
  SupportedLanguages.Go,
72
+ SupportedLanguages.C,
72
73
  ]);
73
74
  /**
74
75
  * Return the env-var name that controls a given language's registry-
@@ -414,6 +414,17 @@ export interface ScopeResolver {
414
414
  * but is too loose as a default for strict module systems.
415
415
  */
416
416
  readonly allowGlobalFreeCallFallback?: boolean;
417
+ /**
418
+ * Optional predicate to identify definitions with file-local linkage
419
+ * (e.g. C `static` functions). When provided, `pickUniqueGlobalCallable`
420
+ * excludes defs where `isFileLocalDef(def) === true` and the def lives
421
+ * in a different file from the caller. This prevents the global free-call
422
+ * fallback from creating CALLS edges to file-local symbols that are
423
+ * logically invisible from the caller's translation unit.
424
+ *
425
+ * Languages without file-local linkage semantics leave this undefined.
426
+ */
427
+ readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
417
428
  /**
418
429
  * Optional post-finalize hook to inject cross-file bindings that
419
430
  * aren't modeled via explicit imports. Runs after
@@ -16,7 +16,7 @@
16
16
  * Generic; promoted from `languages/python/scope-resolver.ts` per the scope-resolution
17
17
  * generalization plan.
18
18
  */
19
- import type { ParsedFile, Reference, ScopeId } from '../../../../_shared/index.js';
19
+ import type { ParsedFile, Reference, ScopeId, SymbolDefinition } from '../../../../_shared/index.js';
20
20
  import type { KnowledgeGraph } from '../../../graph/types.js';
21
21
  import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
22
22
  import type { SemanticModel } from '../../model/semantic-model.js';
@@ -26,4 +26,5 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
26
26
  readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
27
27
  }, handledSites: Set<string>, model: SemanticModel, workspaceIndex: WorkspaceResolutionIndex, options?: {
28
28
  readonly allowGlobalFallback?: boolean;
29
+ readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
29
30
  }): number;
@@ -55,7 +55,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
55
55
  // the caller does not import the target package. Same-package calls are
56
56
  // caught by findCallableBindingInScope above before reaching here.
57
57
  if (fnDef === undefined && options.allowGlobalFallback === true) {
58
- fnDef = pickUniqueGlobalCallable(site.name, model, scopes);
58
+ fnDef = pickUniqueGlobalCallable(site.name, model, scopes, parsed.filePath, options.isFileLocalDef);
59
59
  }
60
60
  if (fnDef === undefined)
61
61
  continue;
@@ -88,7 +88,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
88
88
  }
89
89
  return emitted;
90
90
  }
91
- function pickUniqueGlobalCallable(name, model, scopes) {
91
+ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLocalDef) {
92
92
  const scopeDefs = [];
93
93
  const scopeSeen = new Set();
94
94
  for (const def of scopes.defs.byId.values()) {
@@ -97,6 +97,11 @@ function pickUniqueGlobalCallable(name, model, scopes) {
97
97
  continue;
98
98
  if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
99
99
  continue;
100
+ // Skip file-local defs (e.g. C `static` functions) that live in a
101
+ // different file from the caller — they are logically invisible.
102
+ if (isFileLocalDef !== undefined && def.filePath !== callerFilePath && isFileLocalDef(def)) {
103
+ continue;
104
+ }
100
105
  const key = logicalCallableKey(def);
101
106
  if (scopeSeen.has(key))
102
107
  continue;
@@ -109,6 +114,12 @@ function pickUniqueGlobalCallable(name, model, scopes) {
109
114
  const seen = new Set();
110
115
  const push = (pool) => {
111
116
  for (const def of pool) {
117
+ // Apply the same file-local linkage filter as Phase 1 —
118
+ // cross-file static defs must never leak through the
119
+ // SemanticModel fallback path.
120
+ if (isFileLocalDef !== undefined && def.filePath !== callerFilePath && isFileLocalDef(def)) {
121
+ continue;
122
+ }
112
123
  const key = logicalCallableKey(def);
113
124
  if (seen.has(key))
114
125
  continue;
@@ -13,6 +13,7 @@ import { pythonScopeResolver } from '../../languages/python/scope-resolver.js';
13
13
  import { csharpScopeResolver } from '../../languages/csharp/scope-resolver.js';
14
14
  import { typescriptScopeResolver } from '../../languages/typescript/scope-resolver.js';
15
15
  import { goScopeResolver } from '../../languages/go/scope-resolver.js';
16
+ import { cScopeResolver } from '../../languages/c/scope-resolver.js';
16
17
  /** Map of `SupportedLanguages` → `ScopeResolver`. The phase iterates
17
18
  * this map intersected with `MIGRATED_LANGUAGES` (the per-language
18
19
  * flag set) so adding a resolver here without flipping the flag is
@@ -22,4 +23,5 @@ export const SCOPE_RESOLVERS = new Map([
22
23
  [SupportedLanguages.CSharp, csharpScopeResolver],
23
24
  [SupportedLanguages.TypeScript, typescriptScopeResolver],
24
25
  [SupportedLanguages.Go, goScopeResolver],
26
+ [SupportedLanguages.C, cScopeResolver],
25
27
  ]);
@@ -169,7 +169,10 @@ export function runScopeResolution(input, provider) {
169
169
  // ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ────────────
170
170
  const handledSites = new Set();
171
171
  const receiverExtras = emitReceiverBoundCalls(graph, indexes, parsedFiles, nodeLookup, handledSites, provider, workspaceIndex, readonlyModel);
172
- const freeCallExtras = emitFreeCallFallback(graph, indexes, parsedFiles, nodeLookup, referenceIndex, handledSites, readonlyModel, workspaceIndex, { allowGlobalFallback: provider.allowGlobalFreeCallFallback === true });
172
+ const freeCallExtras = emitFreeCallFallback(graph, indexes, parsedFiles, nodeLookup, referenceIndex, handledSites, readonlyModel, workspaceIndex, {
173
+ allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
174
+ isFileLocalDef: provider.isFileLocalDef,
175
+ });
173
176
  const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
174
177
  const importsEmitted = emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason);
175
178
  if (PROF) {
@@ -10,6 +10,7 @@ import os from 'os';
10
10
  import fs from 'fs/promises';
11
11
  import { isIP } from 'net';
12
12
  import { logger } from '../core/logger.js';
13
+ import { parseRepoNameFromUrl } from '../storage/git.js';
13
14
  /** Root directory for all cloned repositories. Targets must resolve inside this. */
14
15
  const CLONE_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
15
16
  // A valid git repository name is filesystem-safe: alphanumerics plus `. _ -`.
@@ -25,19 +26,15 @@ const REPO_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
25
26
  * clone root via path traversal.
26
27
  */
27
28
  export function extractRepoName(url) {
28
- // Strip trailing slashes without a regex to avoid polynomial-ReDoS on
29
- // pathological inputs like `https://x.com/y` + '/'.repeat(1e6). CodeQL's
30
- // js/polynomial-redos flagged `/\/+$/` here.
31
- let end = url.length;
32
- while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */)
33
- end--;
34
- const cleaned = url.slice(0, end);
35
- const lastSegment = cleaned.split(/[/:]/).pop() || '';
36
- const stripped = lastSegment.endsWith('.git') ? lastSegment.slice(0, -4) : lastSegment;
37
- if (!stripped || stripped === '.' || stripped === '..' || !REPO_NAME_PATTERN.test(stripped)) {
29
+ const name = parseRepoNameFromUrl(url);
30
+ if (!name ||
31
+ name === '.' ||
32
+ name === '..' ||
33
+ name === 'unknown' ||
34
+ !REPO_NAME_PATTERN.test(name)) {
38
35
  throw new Error('Could not extract a valid repository name from URL');
39
36
  }
40
- return stripped;
37
+ return name;
41
38
  }
42
39
  /** Get the clone target directory for a repo name. */
43
40
  export function getCloneDir(repoName) {
@@ -347,8 +344,8 @@ export async function cloneOrPull(url, targetDir, onProgress) {
347
344
  throw new Error(`Clone target must be a subdirectory of ${CLONE_ROOT}`);
348
345
  }
349
346
  // Always validate the requested URL — the prior shape only ran this in
350
- // the clone branch, leaving the pull branch as an SSRF / blocked-host
351
- // bypass when an existing clone shared the basename of an attacker URL.
347
+ // the code path where the repo was cloned. Now it runs unconditionally,
348
+ // preventing SSRF / blocked-host bypasses even when targetDir already exists.
352
349
  validateGitUrl(url);
353
350
  const exists = await fs.access(path.join(safeTarget, '.git')).then(() => true, () => false);
354
351
  if (exists) {
@@ -108,13 +108,24 @@ export declare const hasGitDir: (dirPath: string) => boolean;
108
108
  */
109
109
  export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
110
110
  /**
111
- * Parse a repository name out of a git remote URL. Handles the common
112
- * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
113
- * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
114
- * unparseable input.
111
+ * Sanitize a repository name to prevent argument injection and ensure
112
+ * cross-platform filesystem compatibility.
115
113
  *
116
- * The heuristic: strip a trailing `.git` and trailing slashes, then
117
- * take the segment after the last `/` or `:`.
114
+ * 1. Strips leading dashes to prevent git command-line argument injection
115
+ * (e.g., --upload-pack=evil).
116
+ * 2. Replaces characters that are unsafe for directory names across
117
+ * platforms (Windows/macOS/Linux) with underscores.
118
+ * 3. Blocks path traversal segments ("." and "..") and Windows reserved
119
+ * names (e.g., CON, NUL) to prevent directory escape.
120
+ */
121
+ export declare const sanitizeRepoName: (name: string) => string;
122
+ /**
123
+ * Parse a repository name out of a git remote URL. Handles common shapes
124
+ * including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
125
+ *
126
+ * Returns a sanitized, filesystem-safe name or null if no name could be inferred.
127
+ * Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
128
+ * for fallbacks without risk of registry collisions on 'unknown'.
118
129
  */
119
130
  export declare const parseRepoNameFromUrl: (url: string | null | undefined) => string | null;
120
131
  /**
@@ -257,13 +257,36 @@ export const getRemoteOriginUrl = (repoPath) => {
257
257
  }
258
258
  };
259
259
  /**
260
- * Parse a repository name out of a git remote URL. Handles the common
261
- * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
262
- * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
263
- * unparseable input.
260
+ * Sanitize a repository name to prevent argument injection and ensure
261
+ * cross-platform filesystem compatibility.
264
262
  *
265
- * The heuristic: strip a trailing `.git` and trailing slashes, then
266
- * take the segment after the last `/` or `:`.
263
+ * 1. Strips leading dashes to prevent git command-line argument injection
264
+ * (e.g., --upload-pack=evil).
265
+ * 2. Replaces characters that are unsafe for directory names across
266
+ * platforms (Windows/macOS/Linux) with underscores.
267
+ * 3. Blocks path traversal segments ("." and "..") and Windows reserved
268
+ * names (e.g., CON, NUL) to prevent directory escape.
269
+ */
270
+ export const sanitizeRepoName = (name) => {
271
+ // 1. Prevent argument injection by stripping leading dashes.
272
+ // 2. Remove characters that are not alphanumerics, dots, underscores, or dashes.
273
+ const sanitized = name.replace(/^-+/, '').replace(/[^a-zA-Z0-9._-]/g, '_');
274
+ // 3. Block path traversal segments and Windows reserved names.
275
+ // Windows reserved names like CON, PRN, AUX, NUL, COM1-9, LPT1-9 cannot
276
+ // be used as directory names on Windows even if they have an extension.
277
+ const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
278
+ if (!sanitized || sanitized === '.' || sanitized === '..' || reserved.test(sanitized)) {
279
+ return 'unknown';
280
+ }
281
+ return sanitized;
282
+ };
283
+ /**
284
+ * Parse a repository name out of a git remote URL. Handles common shapes
285
+ * including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
286
+ *
287
+ * Returns a sanitized, filesystem-safe name or null if no name could be inferred.
288
+ * Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
289
+ * for fallbacks without risk of registry collisions on 'unknown'.
267
290
  */
268
291
  export const parseRepoNameFromUrl = (url) => {
269
292
  if (!url)
@@ -271,12 +294,23 @@ export const parseRepoNameFromUrl = (url) => {
271
294
  const trimmed = url.trim();
272
295
  if (!trimmed)
273
296
  return null;
274
- // Strip `.git` suffix (case-insensitive) and any trailing slashes.
275
- const withoutSuffix = trimmed.replace(/\.git\/*$/i, '').replace(/\/+$/, '');
276
- // Last path segment, splitting on either `/` or `:` (covers SSH form).
277
- const m = withoutSuffix.match(/[/:]([^/:]+)$/);
278
- const candidate = m ? m[1] : withoutSuffix;
279
- return candidate || null;
297
+ // Strip trailing slashes without a regex to avoid polynomial-ReDoS on
298
+ // pathological inputs like `https://x.com/y` + '/'.repeat(1e6).
299
+ let end = trimmed.length;
300
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47 /* '/' */)
301
+ end--;
302
+ let cleaned = trimmed.slice(0, end);
303
+ // Strip trailing .git (case-insensitive)
304
+ if (cleaned.toLowerCase().endsWith('.git')) {
305
+ cleaned = cleaned.slice(0, -4);
306
+ }
307
+ // Last path segment, handling colons for SSH URLs and path traversal.
308
+ // Split on both / and : to consistently extract the last part.
309
+ const candidate = cleaned.split(/[/:]/).pop() || '';
310
+ if (!candidate)
311
+ return null;
312
+ const safe = sanitizeRepoName(candidate);
313
+ return safe === 'unknown' ? null : safe;
280
314
  };
281
315
  /**
282
316
  * Convenience wrapper: derive a registry-friendly name from the repo's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.4",
3
+ "version": "1.6.5-rc.6",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",