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.
- package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
- package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
- package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
- package/dist/core/ingestion/languages/c/arity.js +18 -0
- package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/c/captures.js +105 -0
- package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
- package/dist/core/ingestion/languages/c/header-scan.js +55 -0
- package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
- package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
- package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
- package/dist/core/ingestion/languages/c/import-target.js +57 -0
- package/dist/core/ingestion/languages/c/index.d.ts +11 -0
- package/dist/core/ingestion/languages/c/index.js +11 -0
- package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
- package/dist/core/ingestion/languages/c/interpret.js +48 -0
- package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
- package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
- package/dist/core/ingestion/languages/c/query.d.ts +3 -0
- package/dist/core/ingestion/languages/c/query.js +161 -0
- package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
- package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
- package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
- package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
- package/dist/core/ingestion/languages/c-cpp.js +10 -0
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +11 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +2 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +13 -2
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +4 -1
- package/dist/server/git-clone.js +10 -13
- package/dist/storage/git.d.ts +17 -6
- package/dist/storage/git.js +46 -12
- 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,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,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,
|
|
@@ -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, {
|
|
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) {
|
package/dist/server/git-clone.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
351
|
-
//
|
|
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) {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
112
|
-
*
|
|
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
|
-
*
|
|
117
|
-
*
|
|
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
|
/**
|
package/dist/storage/git.js
CHANGED
|
@@ -257,13 +257,36 @@ export const getRemoteOriginUrl = (repoPath) => {
|
|
|
257
257
|
}
|
|
258
258
|
};
|
|
259
259
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
-
*
|
|
266
|
-
*
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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