gitnexus 1.6.6-rc.31 → 1.6.6-rc.33
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/cli/optional-grammars.d.ts +11 -8
- package/dist/cli/optional-grammars.js +12 -8
- package/dist/core/group/extractors/http-patterns/python.js +223 -14
- package/dist/core/ingestion/parsing-processor.js +12 -6
- package/dist/core/ingestion/scope-extractor.js +7 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +21 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +7 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +5 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +50 -2
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +28 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +75 -23
- package/dist/core/ingestion/utils/ast-helpers.d.ts +30 -0
- package/dist/core/ingestion/utils/ast-helpers.js +99 -0
- package/dist/core/ingestion/workers/parse-worker.js +12 -6
- package/package.json +3 -6
- package/scripts/build-tree-sitter-dart.cjs +4 -0
- package/scripts/build-tree-sitter-proto.cjs +3 -4
- package/scripts/build-tree-sitter-swift.cjs +39 -0
- package/scripts/materialize-vendor-grammars.cjs +72 -0
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -8
- package/vendor/node_modules/node-addon-api/node_addon_api.Makefile +0 -6
- package/vendor/node_modules/node-addon-api/node_addon_api.target.mk +0 -106
- package/vendor/node_modules/node-addon-api/node_addon_api_except.target.mk +0 -110
- package/vendor/node_modules/node-addon-api/node_addon_api_except_all.target.mk +0 -106
- package/vendor/node_modules/node-addon-api/node_addon_api_maybe.target.mk +0 -106
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional grammar availability check.
|
|
3
3
|
*
|
|
4
|
-
* tree-sitter-dart and tree-sitter-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
|
|
5
|
+
* under vendor/ and materialized into node_modules/ at postinstall. Dart
|
|
6
|
+
* and Proto are built from source with node-gyp; Swift ships platform
|
|
7
|
+
* prebuilds activated via node-gyp-build. All three can be skipped via
|
|
8
|
+
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
|
|
9
|
+
* soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
|
|
10
|
+
* matches the host platform (Swift).
|
|
8
11
|
*
|
|
9
12
|
* Either path produces the same observable: the .node binding is absent
|
|
10
13
|
* at runtime. This helper detects that condition and surfaces a single
|
|
11
|
-
* stderr line per missing grammar so users learn why .dart/.proto
|
|
12
|
-
* is unavailable instead of silently getting a degraded index.
|
|
14
|
+
* stderr line per missing grammar so users learn why .dart/.proto/.swift
|
|
15
|
+
* support is unavailable instead of silently getting a degraded index.
|
|
13
16
|
*/
|
|
14
17
|
export interface MissingGrammar {
|
|
15
18
|
name: string;
|
|
@@ -19,8 +22,8 @@ export interface MissingGrammar {
|
|
|
19
22
|
* Returns the list of optional grammars whose native binding cannot be
|
|
20
23
|
* loaded. Actually `require()`s the package — `require.resolve` would
|
|
21
24
|
* locate the entry path even when the `.node` binding is absent (the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* package directory exists without a working `.node` binding), giving false
|
|
26
|
+
* negatives for the exact users we want to warn:
|
|
24
27
|
* those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
|
|
25
28
|
* native rebuild soft-failed for missing toolchain.
|
|
26
29
|
*
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional grammar availability check.
|
|
3
3
|
*
|
|
4
|
-
* tree-sitter-dart and tree-sitter-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
|
|
5
|
+
* under vendor/ and materialized into node_modules/ at postinstall. Dart
|
|
6
|
+
* and Proto are built from source with node-gyp; Swift ships platform
|
|
7
|
+
* prebuilds activated via node-gyp-build. All three can be skipped via
|
|
8
|
+
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
|
|
9
|
+
* soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
|
|
10
|
+
* matches the host platform (Swift).
|
|
8
11
|
*
|
|
9
12
|
* Either path produces the same observable: the .node binding is absent
|
|
10
13
|
* at runtime. This helper detects that condition and surfaces a single
|
|
11
|
-
* stderr line per missing grammar so users learn why .dart/.proto
|
|
12
|
-
* is unavailable instead of silently getting a degraded index.
|
|
14
|
+
* stderr line per missing grammar so users learn why .dart/.proto/.swift
|
|
15
|
+
* support is unavailable instead of silently getting a degraded index.
|
|
13
16
|
*/
|
|
14
17
|
import { createRequire } from 'module';
|
|
15
18
|
import { cliWarn } from './cli-message.js';
|
|
@@ -17,13 +20,14 @@ const _require = createRequire(import.meta.url);
|
|
|
17
20
|
const OPTIONAL_GRAMMARS = [
|
|
18
21
|
{ name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
|
|
19
22
|
{ name: 'tree-sitter-proto', pkg: 'tree-sitter-proto', extensions: ['.proto'] },
|
|
23
|
+
{ name: 'tree-sitter-swift', pkg: 'tree-sitter-swift', extensions: ['.swift'] },
|
|
20
24
|
];
|
|
21
25
|
/**
|
|
22
26
|
* Returns the list of optional grammars whose native binding cannot be
|
|
23
27
|
* loaded. Actually `require()`s the package — `require.resolve` would
|
|
24
28
|
* locate the entry path even when the `.node` binding is absent (the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
29
|
+
* package directory exists without a working `.node` binding), giving false
|
|
30
|
+
* negatives for the exact users we want to warn:
|
|
27
31
|
* those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
|
|
28
32
|
* native rebuild soft-failed for missing toolchain.
|
|
29
33
|
*
|
|
@@ -5,7 +5,12 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
|
|
|
5
5
|
* - FastAPI `@app.get("/path")` provider decorators
|
|
6
6
|
* - `requests.get/post/...("url")` consumer calls
|
|
7
7
|
* - Generic `requests.request("METHOD", "url")` consumer calls
|
|
8
|
-
* - `httpx.AsyncClient` instances calling `.get/.post/...("url")
|
|
8
|
+
* - `httpx.AsyncClient` instances calling `.get/.post/...("url")`, including
|
|
9
|
+
* aliased imports such as `import httpx as hx`,
|
|
10
|
+
* `from httpx import AsyncClient`, and
|
|
11
|
+
* `from httpx import AsyncClient as HttpxAsyncClient`.
|
|
12
|
+
* Locally rebound names (e.g. `AsyncClient = mock_factory()` inside a
|
|
13
|
+
* function) are excluded to avoid false-positive consumer contracts.
|
|
9
14
|
*/
|
|
10
15
|
const FASTAPI_VERBS = {
|
|
11
16
|
get: 'GET',
|
|
@@ -67,12 +72,48 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
|
|
|
67
72
|
],
|
|
68
73
|
});
|
|
69
74
|
// ─── Consumer: httpx.AsyncClient assignments ────────────────────────
|
|
70
|
-
//
|
|
71
|
-
// construction. Direct imports (`from httpx import AsyncClient`) and module
|
|
72
|
-
// aliases (`import httpx as hx`) and annotated assignments (`client: httpx.AsyncClient = ...`)
|
|
73
|
-
// are intentionally left for a follow-up. Module-scope clients are only matched
|
|
75
|
+
// Module-scope clients are only matched
|
|
74
76
|
// at module scope; calls inside functions require a function/class-local tracked
|
|
75
77
|
// client to avoid false positives from same-name local variables.
|
|
78
|
+
const HTTPX_MODULE_IMPORT_PATTERNS = compilePatterns({
|
|
79
|
+
name: 'python-httpx-module-imports',
|
|
80
|
+
language: Python,
|
|
81
|
+
patterns: [
|
|
82
|
+
{
|
|
83
|
+
meta: {},
|
|
84
|
+
query: `
|
|
85
|
+
(import_statement
|
|
86
|
+
name: (aliased_import
|
|
87
|
+
name: (dotted_name (identifier) @module)
|
|
88
|
+
alias: (identifier) @alias))
|
|
89
|
+
`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
const HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS = compilePatterns({
|
|
94
|
+
name: 'python-httpx-async-client-imports',
|
|
95
|
+
language: Python,
|
|
96
|
+
patterns: [
|
|
97
|
+
{
|
|
98
|
+
meta: {},
|
|
99
|
+
query: `
|
|
100
|
+
(import_from_statement
|
|
101
|
+
module_name: (dotted_name (identifier) @module)
|
|
102
|
+
name: (dotted_name (identifier) @client_class))
|
|
103
|
+
`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
meta: {},
|
|
107
|
+
query: `
|
|
108
|
+
(import_from_statement
|
|
109
|
+
module_name: (dotted_name (identifier) @module)
|
|
110
|
+
name: (aliased_import
|
|
111
|
+
name: (dotted_name (identifier) @client_class)
|
|
112
|
+
alias: (identifier) @alias))
|
|
113
|
+
`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
76
117
|
const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
|
|
77
118
|
name: 'python-httpx-async-client-assign',
|
|
78
119
|
language: Python,
|
|
@@ -84,8 +125,23 @@ const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
|
|
|
84
125
|
left: (_) @client
|
|
85
126
|
right: (call
|
|
86
127
|
function: (attribute
|
|
87
|
-
object: (identifier) @module
|
|
88
|
-
attribute: (identifier) @client_class
|
|
128
|
+
object: (identifier) @module
|
|
129
|
+
attribute: (identifier) @client_class)))
|
|
130
|
+
`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS = compilePatterns({
|
|
135
|
+
name: 'python-httpx-async-client-direct-assign',
|
|
136
|
+
language: Python,
|
|
137
|
+
patterns: [
|
|
138
|
+
{
|
|
139
|
+
meta: {},
|
|
140
|
+
query: `
|
|
141
|
+
(assignment
|
|
142
|
+
left: (_) @client
|
|
143
|
+
right: (call
|
|
144
|
+
function: (identifier) @client_class))
|
|
89
145
|
`,
|
|
90
146
|
},
|
|
91
147
|
],
|
|
@@ -101,8 +157,23 @@ const HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS = compilePatterns({
|
|
|
101
157
|
(as_pattern
|
|
102
158
|
(call
|
|
103
159
|
function: (attribute
|
|
104
|
-
object: (identifier) @module
|
|
105
|
-
attribute: (identifier) @client_class
|
|
160
|
+
object: (identifier) @module
|
|
161
|
+
attribute: (identifier) @client_class))
|
|
162
|
+
(as_pattern_target (identifier) @client))
|
|
163
|
+
`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
const HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS = compilePatterns({
|
|
168
|
+
name: 'python-httpx-async-client-direct-with-alias',
|
|
169
|
+
language: Python,
|
|
170
|
+
patterns: [
|
|
171
|
+
{
|
|
172
|
+
meta: {},
|
|
173
|
+
query: `
|
|
174
|
+
(as_pattern
|
|
175
|
+
(call
|
|
176
|
+
function: (identifier) @client_class)
|
|
106
177
|
(as_pattern_target (identifier) @client))
|
|
107
178
|
`,
|
|
108
179
|
},
|
|
@@ -131,14 +202,120 @@ function trackedClientScopeKey(clientNode) {
|
|
|
131
202
|
return getScopeKey(clientNode.parent, clientNode.text.includes('.'));
|
|
132
203
|
}
|
|
133
204
|
function callScopeKeys(clientNode) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
205
|
+
return [getScopeKey(clientNode.parent, clientNode.text.includes('.'))];
|
|
206
|
+
}
|
|
207
|
+
// Returns the scope key that a rebind of an imported alias would shadow under
|
|
208
|
+
// Python LEGB rules, or `null` when the rebind does not shadow anything that
|
|
209
|
+
// could produce a false-positive consumer detection.
|
|
210
|
+
// - Rebind inside a function/method → that function's scope.
|
|
211
|
+
// - Rebind at module top level → 'module' (shadows the whole file).
|
|
212
|
+
// - Rebind in a class body without an enclosing function → null. Python
|
|
213
|
+
// class attributes do not shadow bare-name lookups inside methods (methods
|
|
214
|
+
// see the module binding, not the class attribute), so we must not poison
|
|
215
|
+
// them.
|
|
216
|
+
function shadowScopeKey(node) {
|
|
217
|
+
let current = node;
|
|
218
|
+
let passedThroughClass = false;
|
|
219
|
+
while (current) {
|
|
220
|
+
if (current.type === 'function_definition') {
|
|
221
|
+
// Reuse getScopeKey's key format so the two helpers cannot drift apart.
|
|
222
|
+
return getScopeKey(current);
|
|
223
|
+
}
|
|
224
|
+
if (current.type === 'class_definition') {
|
|
225
|
+
passedThroughClass = true;
|
|
226
|
+
}
|
|
227
|
+
current = current.parent;
|
|
228
|
+
}
|
|
229
|
+
return passedThroughClass ? null : 'module';
|
|
230
|
+
}
|
|
231
|
+
function collectHttpxImportAliases(tree) {
|
|
232
|
+
const moduleAliases = new Set(['httpx']);
|
|
233
|
+
const asyncClientAliases = new Set();
|
|
234
|
+
// The @module capture is a single identifier inside a `dotted_name`, so for
|
|
235
|
+
// `import package.httpx as hx` the pattern would match the inner `httpx`
|
|
236
|
+
// segment. Check the full `dotted_name` text via `parent` to anchor the match.
|
|
237
|
+
for (const match of runCompiledPatterns(HTTPX_MODULE_IMPORT_PATTERNS, tree)) {
|
|
238
|
+
const moduleNode = match.captures.module;
|
|
239
|
+
const aliasNode = match.captures.alias;
|
|
240
|
+
if (moduleNode?.parent?.text === 'httpx' && aliasNode)
|
|
241
|
+
moduleAliases.add(aliasNode.text);
|
|
242
|
+
}
|
|
243
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS, tree)) {
|
|
244
|
+
const moduleNode = match.captures.module;
|
|
245
|
+
const classNode = match.captures.client_class;
|
|
246
|
+
if (moduleNode?.parent?.text !== 'httpx' || classNode?.text !== 'AsyncClient')
|
|
247
|
+
continue;
|
|
248
|
+
asyncClientAliases.add(match.captures.alias?.text ?? classNode.text);
|
|
249
|
+
}
|
|
250
|
+
return { moduleAliases, asyncClientAliases };
|
|
251
|
+
}
|
|
252
|
+
// Tracks local rebindings (`AsyncClient = ...`, `hx = ...`) that shadow an
|
|
253
|
+
// imported alias. We treat the whole enclosing scope (module, class, or
|
|
254
|
+
// function) as shadowed for that alias name, so subsequent constructions in
|
|
255
|
+
// that scope are not falsely detected as httpx consumers. Covers bare-identifier
|
|
256
|
+
// targets and the common tuple / list destructuring shapes.
|
|
257
|
+
const ALIAS_SHADOW_PATTERNS = compilePatterns({
|
|
258
|
+
name: 'python-httpx-alias-shadow',
|
|
259
|
+
language: Python,
|
|
260
|
+
patterns: [
|
|
261
|
+
{
|
|
262
|
+
meta: {},
|
|
263
|
+
query: `(assignment left: (identifier) @name)`,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
meta: {},
|
|
267
|
+
query: `(assignment left: (pattern_list (identifier) @name))`,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
meta: {},
|
|
271
|
+
query: `(assignment left: (tuple_pattern (identifier) @name))`,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
meta: {},
|
|
275
|
+
query: `(assignment left: (list_pattern (identifier) @name))`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
function collectAliasShadowScopes(tree, aliases) {
|
|
280
|
+
const shadowed = new Map();
|
|
281
|
+
if (aliases.size === 0)
|
|
282
|
+
return shadowed;
|
|
283
|
+
for (const match of runCompiledPatterns(ALIAS_SHADOW_PATTERNS, tree)) {
|
|
284
|
+
const nameNode = match.captures.name;
|
|
285
|
+
if (!nameNode || !aliases.has(nameNode.text))
|
|
286
|
+
continue;
|
|
287
|
+
const scopeKey = shadowScopeKey(nameNode.parent);
|
|
288
|
+
if (scopeKey === null)
|
|
289
|
+
continue;
|
|
290
|
+
const set = shadowed.get(nameNode.text) ?? new Set();
|
|
291
|
+
set.add(scopeKey);
|
|
292
|
+
shadowed.set(nameNode.text, set);
|
|
293
|
+
}
|
|
294
|
+
return shadowed;
|
|
295
|
+
}
|
|
296
|
+
function isAliasShadowed(shadowed, aliasName, node) {
|
|
297
|
+
const scopes = shadowed.get(aliasName);
|
|
298
|
+
if (!scopes || scopes.size === 0)
|
|
299
|
+
return false;
|
|
300
|
+
let current = node.parent;
|
|
301
|
+
while (current) {
|
|
302
|
+
if (current.type === 'function_definition') {
|
|
303
|
+
// Reuse getScopeKey's key format so the two helpers cannot drift apart.
|
|
304
|
+
if (scopes.has(getScopeKey(current)))
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
current = current.parent;
|
|
308
|
+
}
|
|
309
|
+
// A module-level rebind shadows the alias for the entire file.
|
|
310
|
+
return scopes.has('module');
|
|
139
311
|
}
|
|
140
312
|
function collectHttpxAsyncClients(tree) {
|
|
141
313
|
const clients = new Map();
|
|
314
|
+
const { moduleAliases, asyncClientAliases } = collectHttpxImportAliases(tree);
|
|
315
|
+
// Module aliases (`hx`) and AsyncClient aliases (`AsyncClient`,
|
|
316
|
+
// `HttpxAsyncClient`) share disjoint name spaces, so one shadow map keyed by
|
|
317
|
+
// alias name serves both lookups and we only walk the tree for rebinds once.
|
|
318
|
+
const shadowed = collectAliasShadowScopes(tree, new Set([...moduleAliases, ...asyncClientAliases]));
|
|
142
319
|
const addClient = (clientNode) => {
|
|
143
320
|
if (!clientNode)
|
|
144
321
|
return;
|
|
@@ -149,9 +326,41 @@ function collectHttpxAsyncClients(tree) {
|
|
|
149
326
|
clients.set(clientText, scopes);
|
|
150
327
|
};
|
|
151
328
|
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS, tree)) {
|
|
329
|
+
const moduleNode = match.captures.module;
|
|
330
|
+
const classNode = match.captures.client_class;
|
|
331
|
+
if (!moduleNode || !classNode)
|
|
332
|
+
continue;
|
|
333
|
+
if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
|
|
334
|
+
continue;
|
|
335
|
+
if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
|
|
336
|
+
continue;
|
|
337
|
+
addClient(match.captures.client);
|
|
338
|
+
}
|
|
339
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS, tree)) {
|
|
340
|
+
const classNode = match.captures.client_class;
|
|
341
|
+
if (!classNode || !asyncClientAliases.has(classNode.text))
|
|
342
|
+
continue;
|
|
343
|
+
if (isAliasShadowed(shadowed, classNode.text, classNode))
|
|
344
|
+
continue;
|
|
152
345
|
addClient(match.captures.client);
|
|
153
346
|
}
|
|
154
347
|
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS, tree)) {
|
|
348
|
+
const moduleNode = match.captures.module;
|
|
349
|
+
const classNode = match.captures.client_class;
|
|
350
|
+
if (!moduleNode || !classNode)
|
|
351
|
+
continue;
|
|
352
|
+
if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
|
|
353
|
+
continue;
|
|
354
|
+
if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
|
|
355
|
+
continue;
|
|
356
|
+
addClient(match.captures.client);
|
|
357
|
+
}
|
|
358
|
+
for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS, tree)) {
|
|
359
|
+
const classNode = match.captures.client_class;
|
|
360
|
+
if (!classNode || !asyncClientAliases.has(classNode.text))
|
|
361
|
+
continue;
|
|
362
|
+
if (isAliasShadowed(shadowed, classNode.text, classNode))
|
|
363
|
+
continue;
|
|
155
364
|
addClient(match.captures.client);
|
|
156
365
|
}
|
|
157
366
|
return clients;
|
|
@@ -7,7 +7,7 @@ import { extractVueScript, isVueSetupTopLevel } from './vue-sfc-extractor.js';
|
|
|
7
7
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
8
8
|
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
|
9
9
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
10
|
-
import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
|
|
10
|
+
import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
|
|
11
11
|
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
12
12
|
import { buildTypeEnv } from './type-env.js';
|
|
13
13
|
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
|
|
@@ -400,6 +400,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
400
400
|
? cachedFindEnclosingClassInfo(nameNode || definitionNodeForRange, file.path, provider.resolveEnclosingOwner)
|
|
401
401
|
: null;
|
|
402
402
|
const enclosingClassId = enclosingClassInfo?.classId ?? null;
|
|
403
|
+
const objectLiteralOwnerInfo = !enclosingClassId && nodeLabel === 'Method' && definitionNode
|
|
404
|
+
? findObjectLiteralBindingInfo(definitionNode, file.path)
|
|
405
|
+
: null;
|
|
403
406
|
// Qualify method/property IDs with enclosing class name to avoid collisions
|
|
404
407
|
// e.g. "Method:animal.dart:Animal.speak" vs "Method:animal.dart:Dog.speak"
|
|
405
408
|
const qualifiedName = enclosingClassInfo
|
|
@@ -615,7 +618,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
615
618
|
returnType: methodProps.returnType,
|
|
616
619
|
declaredType,
|
|
617
620
|
templateArguments: classTemplateArguments,
|
|
618
|
-
ownerId: enclosingClassId ?? undefined,
|
|
621
|
+
ownerId: enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? undefined,
|
|
619
622
|
qualifiedName: qualifiedTypeName,
|
|
620
623
|
});
|
|
621
624
|
const fileId = generateId('File', file.path);
|
|
@@ -630,15 +633,18 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
630
633
|
};
|
|
631
634
|
graph.addRelationship(relationship);
|
|
632
635
|
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
|
633
|
-
|
|
636
|
+
const ownerIdForMemberEdge = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? null;
|
|
637
|
+
if (ownerIdForMemberEdge) {
|
|
634
638
|
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
|
635
639
|
graph.addRelationship({
|
|
636
|
-
id: generateId(memberEdgeType, `${
|
|
637
|
-
sourceId:
|
|
640
|
+
id: generateId(memberEdgeType, `${ownerIdForMemberEdge}->${nodeId}`),
|
|
641
|
+
sourceId: ownerIdForMemberEdge,
|
|
638
642
|
targetId: nodeId,
|
|
639
643
|
type: memberEdgeType,
|
|
640
644
|
confidence: 1.0,
|
|
641
|
-
reason:
|
|
645
|
+
reason: objectLiteralOwnerInfo
|
|
646
|
+
? 'object literal method belongs to exported object binding'
|
|
647
|
+
: '',
|
|
642
648
|
});
|
|
643
649
|
}
|
|
644
650
|
});
|
|
@@ -511,8 +511,14 @@ function normalizeNodeLabel(kindStr) {
|
|
|
511
511
|
case 'property':
|
|
512
512
|
return 'Property';
|
|
513
513
|
case 'variable':
|
|
514
|
-
case 'const':
|
|
515
514
|
return 'Variable';
|
|
515
|
+
// `const` / `let` declarations align with the legacy DAG parse phase,
|
|
516
|
+
// which emits `Const` graph nodes via `@definition.const` capture for
|
|
517
|
+
// `lexical_declaration`. Returning `'Const'` here lets resolveDefGraphId's
|
|
518
|
+
// qualified-key path succeed for value receivers without relying on the
|
|
519
|
+
// simple-key fallback (PR #1718 review Finding 1 / 2026-05-21-002 U4).
|
|
520
|
+
case 'const':
|
|
521
|
+
return 'Const';
|
|
516
522
|
case 'typealias':
|
|
517
523
|
case 'type_alias':
|
|
518
524
|
return 'TypeAlias';
|
|
@@ -41,3 +41,24 @@ export declare function tryEmitEdge(graph: KnowledgeGraph, scopes: ScopeResoluti
|
|
|
41
41
|
};
|
|
42
42
|
readonly kind: string;
|
|
43
43
|
}, targetDef: SymbolDefinition, reason: string, seen: Set<string>, confidence?: number, collapseByCallerTarget?: boolean): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Variant of `tryEmitEdge` that takes a pre-resolved target graph id
|
|
46
|
+
* instead of resolving it from a `SymbolDefinition`. Used by the
|
|
47
|
+
* value-receiver-owner bridge (`receiver-bound-calls.ts` Case 5) where
|
|
48
|
+
* the picked owner-indexed method def carries no `qualifiedName` (object
|
|
49
|
+
* literals have no class owner to seed it) and therefore cannot
|
|
50
|
+
* round-trip through `resolveDefGraphId`. The def's `nodeId` IS the
|
|
51
|
+
* canonical graph node id (written by the parse phase), so the caller
|
|
52
|
+
* passes it directly.
|
|
53
|
+
*
|
|
54
|
+
* All other invariants of `tryEmitEdge` apply: dedup key shape, collapse
|
|
55
|
+
* flag honoring, edge-type mapping, caller-id resolution.
|
|
56
|
+
*/
|
|
57
|
+
export declare function tryEmitEdgeWithExplicitTargetId(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, nodeLookup: GraphNodeLookup, site: {
|
|
58
|
+
readonly inScope: ScopeId;
|
|
59
|
+
readonly atRange: {
|
|
60
|
+
startLine: number;
|
|
61
|
+
startCol: number;
|
|
62
|
+
};
|
|
63
|
+
readonly kind: string;
|
|
64
|
+
}, targetGraphId: string, reason: string, seen: Set<string>, confidence?: number, collapseByCallerTarget?: boolean): boolean;
|
|
@@ -77,3 +77,40 @@ export function tryEmitEdge(graph, scopes, nodeLookup, site, targetDef, reason,
|
|
|
77
77
|
});
|
|
78
78
|
return true;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Variant of `tryEmitEdge` that takes a pre-resolved target graph id
|
|
82
|
+
* instead of resolving it from a `SymbolDefinition`. Used by the
|
|
83
|
+
* value-receiver-owner bridge (`receiver-bound-calls.ts` Case 5) where
|
|
84
|
+
* the picked owner-indexed method def carries no `qualifiedName` (object
|
|
85
|
+
* literals have no class owner to seed it) and therefore cannot
|
|
86
|
+
* round-trip through `resolveDefGraphId`. The def's `nodeId` IS the
|
|
87
|
+
* canonical graph node id (written by the parse phase), so the caller
|
|
88
|
+
* passes it directly.
|
|
89
|
+
*
|
|
90
|
+
* All other invariants of `tryEmitEdge` apply: dedup key shape, collapse
|
|
91
|
+
* flag honoring, edge-type mapping, caller-id resolution.
|
|
92
|
+
*/
|
|
93
|
+
export function tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, targetGraphId, reason, seen, confidence = 0.85, collapseByCallerTarget = false) {
|
|
94
|
+
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
|
95
|
+
const edgeType = mapReferenceKindToEdgeType(site.kind);
|
|
96
|
+
if (callerGraphId === undefined)
|
|
97
|
+
return false;
|
|
98
|
+
if (edgeType === undefined)
|
|
99
|
+
return false;
|
|
100
|
+
const useCollapsed = collapseByCallerTarget && edgeType === 'CALLS';
|
|
101
|
+
const dedupKey = useCollapsed
|
|
102
|
+
? `${edgeType}:${callerGraphId}->${targetGraphId}`
|
|
103
|
+
: `${edgeType}:${callerGraphId}->${targetGraphId}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
104
|
+
if (seen.has(dedupKey))
|
|
105
|
+
return false;
|
|
106
|
+
seen.add(dedupKey);
|
|
107
|
+
graph.addRelationship({
|
|
108
|
+
id: `rel:${dedupKey}`,
|
|
109
|
+
sourceId: callerGraphId,
|
|
110
|
+
targetId: targetGraphId,
|
|
111
|
+
type: edgeType,
|
|
112
|
+
confidence,
|
|
113
|
+
reason,
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
@@ -137,5 +137,11 @@ export function isLinkableLabel(label) {
|
|
|
137
137
|
// ACCESSES edges target field nodes (e.g. `user.name = "x"` →
|
|
138
138
|
// ACCESSES edge to User's `name` Variable/Property node).
|
|
139
139
|
label === 'Variable' ||
|
|
140
|
-
label === 'Property'
|
|
140
|
+
label === 'Property' ||
|
|
141
|
+
// Const is linkable so the value-receiver-owner bridge in
|
|
142
|
+
// `receiver-bound-calls.ts` Case 5 can translate the scope-resolution
|
|
143
|
+
// `Variable` def for `export const fooService = {...}` to the canonical
|
|
144
|
+
// `Const:filePath:name` graph node id, against which object-literal
|
|
145
|
+
// method symbols register their `ownerId` (PR #1718 / issue #1358).
|
|
146
|
+
label === 'Const');
|
|
141
147
|
}
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* but not a namespace prefix → compound resolver
|
|
22
22
|
* 7. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
|
|
23
23
|
* MRO walk + `findOwnedMember`
|
|
24
|
+
* 8. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
|
|
25
|
+
* whose `nodeId` is referenced as an `ownerId` in `model.methods`
|
|
26
|
+
* (object-literal services). Last-resort fallback for lowercase
|
|
27
|
+
* receivers with no class-like or type-binding match. Mirrors
|
|
28
|
+
* the legacy DAG bridge in `call-processor.ts`.
|
|
24
29
|
*
|
|
25
30
|
* Reordering or merging cases changes resolution semantics.
|
|
26
31
|
*
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* but not a namespace prefix → compound resolver
|
|
22
22
|
* 7. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
|
|
23
23
|
* MRO walk + `findOwnedMember`
|
|
24
|
+
* 8. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
|
|
25
|
+
* whose `nodeId` is referenced as an `ownerId` in `model.methods`
|
|
26
|
+
* (object-literal services). Last-resort fallback for lowercase
|
|
27
|
+
* receivers with no class-like or type-binding match. Mirrors
|
|
28
|
+
* the legacy DAG bridge in `call-processor.ts`.
|
|
24
29
|
*
|
|
25
30
|
* Reordering or merging cases changes resolution semantics.
|
|
26
31
|
*
|
|
@@ -32,8 +37,8 @@
|
|
|
32
37
|
* resolved to a wrong target.
|
|
33
38
|
*/
|
|
34
39
|
import { collectNamespaceTargets } from '../scope/namespace-targets.js';
|
|
35
|
-
import { findClassBindingInScope, findEnclosingClassDef, findExportedDef, findOwnedMember, findReceiverTypeBinding, isClassLike, } from '../scope/walkers.js';
|
|
36
|
-
import { tryEmitEdge } from '../graph-bridge/edges.js';
|
|
40
|
+
import { findClassBindingInScope, findEnclosingClassDef, findExportedDef, findOwnedMember, findReceiverTypeBinding, findValueBindingInScope, isClassLike, } from '../scope/walkers.js';
|
|
41
|
+
import { tryEmitEdge, tryEmitEdgeWithExplicitTargetId } from '../graph-bridge/edges.js';
|
|
37
42
|
import { resolveCompoundReceiverClass } from '../passes/compound-receiver.js';
|
|
38
43
|
import { resolveDefGraphId } from '../graph-bridge/ids.js';
|
|
39
44
|
import { narrowOverloadCandidates, isOverloadAmbiguousAfterNormalization, } from './overload-narrowing.js';
|
|
@@ -514,6 +519,49 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
514
519
|
}
|
|
515
520
|
}
|
|
516
521
|
}
|
|
522
|
+
// ── Case 5: value-receiver bridge (object-literal services) ──
|
|
523
|
+
// When prior cases couldn't resolve the receiver as a class or
|
|
524
|
+
// type binding, fall back to value-binding resolution. Covers:
|
|
525
|
+
//
|
|
526
|
+
// export const fooService = { getUser(id) {...} };
|
|
527
|
+
// import { fooService } from './service';
|
|
528
|
+
// fooService.getUser(id); // ← resolve here
|
|
529
|
+
//
|
|
530
|
+
// `fooService` is a `Const`/`Variable` (not class-like, no typeBinding
|
|
531
|
+
// for unannotated literals), so Cases 2-4 skip it. Scope-resolution
|
|
532
|
+
// defs for non-class values carry a synthetic id, so we translate to
|
|
533
|
+
// the canonical graph node ID via `resolveDefGraphId` before owner-
|
|
534
|
+
// indexed lookup — the parser writes the graph node ID as `ownerId`
|
|
535
|
+
// on the method symbol-table entry to match.
|
|
536
|
+
//
|
|
537
|
+
// Object-literal methods do not carry a `qualifiedName` (no class
|
|
538
|
+
// owner to seed it), so the picked def cannot round-trip through
|
|
539
|
+
// `tryEmitEdge` → `resolveDefGraphId`. We use
|
|
540
|
+
// `tryEmitEdgeWithExplicitTargetId` instead, passing `picked.nodeId`
|
|
541
|
+
// directly — same dedup-key shape, collapse-flag honoring, and
|
|
542
|
+
// caller resolution as `tryEmitEdge`.
|
|
543
|
+
const valueDef = findValueBindingInScope(site.inScope, receiverName, scopes);
|
|
544
|
+
if (valueDef !== undefined) {
|
|
545
|
+
const ownerGraphId = resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId;
|
|
546
|
+
const picked = pickOverload(ownerGraphId, memberName, site, model, provider);
|
|
547
|
+
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
548
|
+
handledSites.add(siteKey);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (picked !== undefined) {
|
|
552
|
+
const reason = site.kind === 'write' || site.kind === 'read'
|
|
553
|
+
? site.kind
|
|
554
|
+
: picked.filePath !== parsed.filePath
|
|
555
|
+
? 'import-resolved'
|
|
556
|
+
: 'global';
|
|
557
|
+
const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
|
|
558
|
+
const ok = tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, picked.nodeId, reason, seen, confidence, collapse);
|
|
559
|
+
if (ok)
|
|
560
|
+
emitted++;
|
|
561
|
+
handledSites.add(siteKey);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
517
565
|
}
|
|
518
566
|
}
|
|
519
567
|
return emitted;
|
|
@@ -91,6 +91,34 @@ export declare function findReceiverTypeBinding(startScope: ScopeId, receiverNam
|
|
|
91
91
|
* Without (2) we'd miss every cross-file class-receiver call.
|
|
92
92
|
*/
|
|
93
93
|
export declare function findClassBindingInScope(startScope: ScopeId, receiverName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Predicate for value-receiver bridge: the labels for which
|
|
96
|
+
* `reconcileOwnership` registers methods/fields under the def's
|
|
97
|
+
* `nodeId` as the `ownerId`. Explicit allowlist so future NodeLabel
|
|
98
|
+
* additions (Module, Namespace, TypeAlias, EnumMember, etc.) do NOT
|
|
99
|
+
* silently widen the bridge — adding a new ownerable label requires
|
|
100
|
+
* touching both this predicate and `reconcileOwnership`.
|
|
101
|
+
*
|
|
102
|
+
* See: `scope-resolution/pipeline/reconcile-ownership.ts` Property /
|
|
103
|
+
* Variable / Const / Static registration block.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isOwnableValueLabel(t: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Look up a value-binding (Const/Variable/Property/Static) by name in
|
|
108
|
+
* the given scope's chain. Used by the value-receiver-owner bridge
|
|
109
|
+
* for object-literal services such as:
|
|
110
|
+
*
|
|
111
|
+
* export const fooService = { getUser(id) {...} };
|
|
112
|
+
*
|
|
113
|
+
* where `fooService` is a `Const`/`Variable` whose `nodeId` is the
|
|
114
|
+
* `ownerId` of the member method. Neither `findClassBindingInScope`
|
|
115
|
+
* (rejects non-class-like) nor `findReceiverTypeBinding` (no typeBinding
|
|
116
|
+
* for an unannotated literal) finds it.
|
|
117
|
+
*
|
|
118
|
+
* Mirrors `findClassBindingInScope` exactly; only the accepted def-type
|
|
119
|
+
* predicate differs.
|
|
120
|
+
*/
|
|
121
|
+
export declare function findValueBindingInScope(startScope: ScopeId, receiverName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
|
|
94
122
|
/**
|
|
95
123
|
* Look up a callable (Function/Method/Constructor) by name in the
|
|
96
124
|
* given scope's chain. Uses the dual-source pattern (scope.bindings +
|