gitnexus 1.6.13-rc.44 → 1.6.13-rc.46
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/README.md +4 -2
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +5 -4
- package/dist/core/ingestion/languages/python/arity-metadata.js +5 -4
- package/dist/core/ingestion/languages/python/captures.js +32 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +15 -1
- package/dist/core/ingestion/languages/python/receiver-binding.js +87 -41
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -2
- package/dist/core/ingestion/languages/python/scope-resolver.js +44 -3
- package/dist/core/ingestion/languages/python/subtype-dispatch.d.ts +31 -0
- package/dist/core/ingestion/languages/python/subtype-dispatch.js +183 -0
- package/dist/core/ingestion/languages/python.js +3 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +17 -13
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +55 -10
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +5 -4
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +6 -7
- package/dist/core/ingestion/scope-resolution/passes/mro.js +30 -7
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +21 -2
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +227 -2
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +16 -4
- package/dist/mcp/local/local-backend.d.ts +21 -0
- package/dist/mcp/local/local-backend.js +187 -1
- package/dist/mcp/read-only-policy.js +2 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +5 -1
- package/dist/mcp/tools.d.ts +7 -1
- package/dist/mcp/tools.js +97 -1
- package/dist/server/grep-params.js +4 -0
- package/dist/storage/parse-cache.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ Note that the bundled Graphology path is no longer the slow option it once was:
|
|
|
210
210
|
|
|
211
211
|
## MCP Tools
|
|
212
212
|
|
|
213
|
-
Your AI agent gets **
|
|
213
|
+
Your AI agent gets **19 tools** (17 per-repo + 2 group) automatically:
|
|
214
214
|
|
|
215
215
|
| Tool | What It Does |
|
|
216
216
|
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -229,10 +229,12 @@ Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
|
|
|
229
229
|
| `api_impact` | Pre-change impact report for an API route handler |
|
|
230
230
|
| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) |
|
|
231
231
|
| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) |
|
|
232
|
+
| `read_file` | Read a checkout file (optional 0-indexed slice; `maxLines` cap) |
|
|
233
|
+
| `grep` | Regex search of the working tree for indexed files (1-based hits; optional `caseSensitive` / `literal`) |
|
|
232
234
|
| `group_list` | List configured repository groups |
|
|
233
235
|
| `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
|
|
234
236
|
|
|
235
|
-
> Read-only tools can omit `repo` when one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing into an unindexed nested Git checkout. Otherwise—and for mutating tools with multiple indexed repos and no MCP default—specify it explicitly: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch
|
|
237
|
+
> Read-only tools can omit `repo` when one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing into an unindexed nested Git checkout. Otherwise—and for mutating tools with multiple indexed repos and no MCP default—specify it explicitly: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch`, except `read_file` and `grep`, which read the checkout and do not accept `branch`. Omitting `branch` queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
|
|
236
238
|
|
|
237
239
|
## MCP Resources
|
|
238
240
|
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Mirrors the legacy `buildMethodProps` conversion so scope-extracted
|
|
7
7
|
* defs carry the same arity semantics as the parse-worker path:
|
|
8
|
-
* -
|
|
8
|
+
* - A bound method's first positional receiver is stripped by class and
|
|
9
|
+
* decorator context, independent of spelling; static/free functions keep it.
|
|
9
10
|
* - Defaulted params contribute to `optionalCount`, flipping
|
|
10
11
|
* `requiredParameterCount = total − optionalCount`.
|
|
11
|
-
* - Variadic (`*args` / `**kwargs`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* - Variadic (`*args` / `**kwargs`) leaves both count-only bounds unknown:
|
|
13
|
+
* parameter kinds are not retained, so a required keyword-only argument
|
|
14
|
+
* cannot safely be treated as a positional minimum.
|
|
14
15
|
* - `parameterTypes` is populated only with real type text, matching
|
|
15
16
|
* legacy behavior.
|
|
16
17
|
*/
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Mirrors the legacy `buildMethodProps` conversion so scope-extracted
|
|
7
7
|
* defs carry the same arity semantics as the parse-worker path:
|
|
8
|
-
* -
|
|
8
|
+
* - A bound method's first positional receiver is stripped by class and
|
|
9
|
+
* decorator context, independent of spelling; static/free functions keep it.
|
|
9
10
|
* - Defaulted params contribute to `optionalCount`, flipping
|
|
10
11
|
* `requiredParameterCount = total − optionalCount`.
|
|
11
|
-
* - Variadic (`*args` / `**kwargs`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* - Variadic (`*args` / `**kwargs`) leaves both count-only bounds unknown:
|
|
13
|
+
* parameter kinds are not retained, so a required keyword-only argument
|
|
14
|
+
* cannot safely be treated as a positional minimum.
|
|
14
15
|
* - `parameterTypes` is populated only with real type text, matching
|
|
15
16
|
* legacy behavior.
|
|
16
17
|
*/
|
|
@@ -27,6 +27,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
|
27
27
|
import { pythonFunctionDefinitionLabel } from './simple-hooks.js';
|
|
28
28
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
29
29
|
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
|
30
|
+
import { beginPythonSubtypeDispatchCapture, recordPythonSimplePositionalCall, recordPythonSubtypeMethodShape, } from './subtype-dispatch.js';
|
|
30
31
|
const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
|
|
31
32
|
functionNodeTypes: new Set(['function_definition', 'lambda']),
|
|
32
33
|
callNodeTypes: new Set(['call']),
|
|
@@ -58,6 +59,7 @@ const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
|
|
|
58
59
|
},
|
|
59
60
|
};
|
|
60
61
|
export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, sourceMeta) {
|
|
62
|
+
beginPythonSubtypeDispatchCapture(filePath);
|
|
61
63
|
let parseText = sourceText;
|
|
62
64
|
let tree = cachedTree;
|
|
63
65
|
let notebookSegments;
|
|
@@ -69,6 +71,9 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
|
|
|
69
71
|
tree = resolved.tree;
|
|
70
72
|
notebookSegments = resolved.notebookSegments;
|
|
71
73
|
}
|
|
74
|
+
const subtypeLineMapper = notebookSegments === undefined
|
|
75
|
+
? undefined
|
|
76
|
+
: (line) => mapExtractLine(line - 1, notebookSegments) + 1;
|
|
72
77
|
// Skip the parse when the caller (the scope-resolution orchestrator's
|
|
73
78
|
// `treeCache`) already produced a Tree for this source — empty under
|
|
74
79
|
// worker-pool runs, so cache miss = re-parse. The cachedTree parameter
|
|
@@ -116,6 +121,7 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
|
|
|
116
121
|
}
|
|
117
122
|
if (Object.keys(grouped).length === 0)
|
|
118
123
|
continue;
|
|
124
|
+
recordPythonSubtypeCallShape(grouped, nodeMap, filePath, subtypeLineMapper);
|
|
119
125
|
if (grouped['@import.statement'] !== undefined) {
|
|
120
126
|
// `@import.statement` is captured directly ON the `import_statement` /
|
|
121
127
|
// `import_from_statement` node (query: `(import_statement) @import.statement`
|
|
@@ -183,6 +189,7 @@ export function emitPythonScopeCaptures(sourceText, filePath, cachedTree, source
|
|
|
183
189
|
if (pythonFunctionDefinitionLabel(fnNode, 'Function') === 'Method') {
|
|
184
190
|
delete grouped['@declaration.function'];
|
|
185
191
|
grouped['@declaration.method'] = { ...anchorCap, name: '@declaration.method' };
|
|
192
|
+
recordPythonSubtypeMethodShape(filePath, fnNode, subtypeLineMapper);
|
|
186
193
|
}
|
|
187
194
|
const arity = computePythonArityMetadata(fnNode);
|
|
188
195
|
if (arity.parameterCount !== undefined) {
|
|
@@ -257,6 +264,31 @@ function remapCaptureMatch(match, segments) {
|
|
|
257
264
|
}
|
|
258
265
|
return next;
|
|
259
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Record fixed positional argument counts only for Python's conservative
|
|
269
|
+
* missing-member subtype fallback. Ordinary reference arity stays unchanged:
|
|
270
|
+
* count-only metadata cannot model Python keyword binding or definition order.
|
|
271
|
+
*/
|
|
272
|
+
function recordPythonSubtypeCallShape(grouped, nodeMap, filePath, mapLine) {
|
|
273
|
+
const callTag = ['@reference.call.free', '@reference.call.member'].find((tag) => grouped[tag] !== undefined);
|
|
274
|
+
if (callTag === undefined)
|
|
275
|
+
return;
|
|
276
|
+
// Decorator references use the same call tags but are anchored on a
|
|
277
|
+
// `decorator`, not a `call`, so they intentionally retain their old shape.
|
|
278
|
+
const callNode = nodeMap[callTag];
|
|
279
|
+
if (callNode === undefined || callNode.type !== 'call')
|
|
280
|
+
return;
|
|
281
|
+
const argumentList = callNode.childForFieldName('arguments');
|
|
282
|
+
if (argumentList === null || argumentList.type !== 'argument_list')
|
|
283
|
+
return;
|
|
284
|
+
const args = argumentList.namedChildren.filter((child) => child !== null && child.type !== 'comment');
|
|
285
|
+
if (args.some((arg) => arg.type === 'list_splat' ||
|
|
286
|
+
arg.type === 'dictionary_splat' ||
|
|
287
|
+
arg.type === 'keyword_argument')) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
recordPythonSimplePositionalCall(filePath, callNode, args.length, mapLine);
|
|
291
|
+
}
|
|
260
292
|
/**
|
|
261
293
|
* Synthesize `@reference.inherits` captures from Python class superclass
|
|
262
294
|
* lists so the registry-primary scope-resolution path emits EXTENDS edges
|
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
* for methods.
|
|
4
4
|
*
|
|
5
5
|
* Tree-sitter can't easily express "the first parameter of a function
|
|
6
|
-
* defined
|
|
6
|
+
* defined in a class suite, including conditional suite branches" via a
|
|
7
|
+
* single static query.
|
|
7
8
|
* Doing this in code keeps the embedded scope query declarative and
|
|
8
9
|
* lets us encode the `@classmethod` / `@staticmethod` decorator
|
|
9
10
|
* awareness that Python's runtime depends on.
|
|
10
11
|
*/
|
|
11
12
|
import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
12
13
|
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
14
|
+
export interface PythonBoundReceiver {
|
|
15
|
+
readonly kind: 'instance' | 'class';
|
|
16
|
+
readonly parameter: SyntaxNode;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly className: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify the parameter that Python's descriptor protocol binds implicitly.
|
|
22
|
+
* Class-suite control flow does not change descriptor ownership, while an
|
|
23
|
+
* intervening function does. Splat and keyword-only parameters cannot name
|
|
24
|
+
* the injected receiver directly and therefore fail closed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyPythonBoundReceiver(fnNode: SyntaxNode): PythonBoundReceiver | null;
|
|
13
27
|
/**
|
|
14
28
|
* Build a `@type-binding.self` (instance method) or `@type-binding.cls`
|
|
15
29
|
* (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* for methods.
|
|
4
4
|
*
|
|
5
5
|
* Tree-sitter can't easily express "the first parameter of a function
|
|
6
|
-
* defined
|
|
6
|
+
* defined in a class suite, including conditional suite branches" via a
|
|
7
|
+
* single static query.
|
|
7
8
|
* Doing this in code keeps the embedded scope query declarative and
|
|
8
9
|
* lets us encode the `@classmethod` / `@staticmethod` decorator
|
|
9
10
|
* awareness that Python's runtime depends on.
|
|
@@ -43,75 +44,120 @@ function hasDecorator(fnNode, decoratorName) {
|
|
|
43
44
|
}
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
+
function firstBoundReceiverParameter(parameters) {
|
|
47
48
|
for (let i = 0; i < parameters.namedChildCount; i++) {
|
|
48
49
|
const child = parameters.namedChild(i);
|
|
49
50
|
if (child === null)
|
|
50
51
|
continue;
|
|
51
|
-
|
|
52
|
-
if (child.type === 'positional_separator' || child.type === 'keyword_separator')
|
|
52
|
+
if (child.type === 'comment')
|
|
53
53
|
continue;
|
|
54
|
-
|
|
54
|
+
// A positional-only separator follows at least one real positional
|
|
55
|
+
// parameter, so encountering it before a candidate is malformed input.
|
|
56
|
+
// A keyword-only separator, *args, or **kwargs means there is no variable
|
|
57
|
+
// that directly receives Python's descriptor-injected instance.
|
|
58
|
+
if (child.type === 'positional_separator' ||
|
|
59
|
+
child.type === 'keyword_separator' ||
|
|
60
|
+
child.type === 'list_splat_pattern' ||
|
|
61
|
+
child.type === 'dictionary_splat_pattern') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return firstParameterName(child) === null ? null : child;
|
|
55
65
|
}
|
|
56
66
|
return null;
|
|
57
67
|
}
|
|
58
68
|
function firstParameterName(param) {
|
|
59
69
|
if (param.type === 'identifier')
|
|
60
70
|
return param.text;
|
|
61
|
-
// typed_parameter / default_parameter / typed_default_parameter
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
function findIdentifierChild(node) {
|
|
67
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
68
|
-
const child = node.namedChild(i);
|
|
69
|
-
if (child !== null && child.type === 'identifier')
|
|
70
|
-
return child;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
71
|
+
// typed_parameter / default_parameter / typed_default_parameter must name a
|
|
72
|
+
// real positional variable. In particular, do not look through a typed
|
|
73
|
+
// list_splat_pattern or dictionary_splat_pattern for its nested identifier.
|
|
74
|
+
const named = param.childForFieldName('name') ?? param.firstNamedChild;
|
|
75
|
+
return named?.type === 'identifier' ? named.text : null;
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* The caller is responsible for guaranteeing `fnNode.type ===
|
|
80
|
-
* 'function_definition'`.
|
|
78
|
+
* Classify the parameter that Python's descriptor protocol binds implicitly.
|
|
79
|
+
* Class-suite control flow does not change descriptor ownership, while an
|
|
80
|
+
* intervening function does. Splat and keyword-only parameters cannot name
|
|
81
|
+
* the injected receiver directly and therefore fail closed.
|
|
81
82
|
*/
|
|
82
|
-
export function
|
|
83
|
+
export function classifyPythonBoundReceiver(fnNode) {
|
|
83
84
|
const enclosingClass = findEnclosingClassDefinition(fnNode);
|
|
84
|
-
if (enclosingClass === null)
|
|
85
|
+
if (enclosingClass === null || hasDecorator(fnNode, 'staticmethod'))
|
|
85
86
|
return null;
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const functionName = fnNode.childForFieldName('name')?.text;
|
|
88
|
+
// Python applies these descriptor kinds implicitly even without decorators.
|
|
89
|
+
// __new__ is static-like (its class argument is explicit), while
|
|
90
|
+
// __init_subclass__ and __class_getitem__ receive the class implicitly.
|
|
91
|
+
if (functionName === '__new__')
|
|
88
92
|
return null;
|
|
89
|
-
const isClassmethod = hasDecorator(fnNode, 'classmethod');
|
|
90
93
|
const params = fnNode.childForFieldName('parameters');
|
|
91
94
|
if (params === null)
|
|
92
95
|
return null;
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
96
|
+
const parameter = firstBoundReceiverParameter(params);
|
|
97
|
+
if (parameter === null)
|
|
98
|
+
return null;
|
|
99
|
+
const name = firstParameterName(parameter);
|
|
100
|
+
const className = classDefinitionName(enclosingClass);
|
|
101
|
+
if (name === null || className === null)
|
|
102
|
+
return null;
|
|
103
|
+
return {
|
|
104
|
+
kind: hasDecorator(fnNode, 'classmethod') ||
|
|
105
|
+
functionName === '__init_subclass__' ||
|
|
106
|
+
functionName === '__class_getitem__'
|
|
107
|
+
? 'class'
|
|
108
|
+
: 'instance',
|
|
109
|
+
parameter,
|
|
110
|
+
name,
|
|
111
|
+
className,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `__new__` is static-like for method dispatch, but Python supplies its class
|
|
116
|
+
* argument during construction rather than through descriptor binding. Keep
|
|
117
|
+
* that explicit parameter in arity metadata while still typing its local name.
|
|
118
|
+
*/
|
|
119
|
+
function classifyPythonExplicitNewReceiver(fnNode) {
|
|
120
|
+
if (fnNode.childForFieldName('name')?.text !== '__new__')
|
|
121
|
+
return null;
|
|
122
|
+
const enclosingClass = findEnclosingClassDefinition(fnNode);
|
|
123
|
+
const params = fnNode.childForFieldName('parameters');
|
|
124
|
+
if (enclosingClass === null || params === null)
|
|
125
|
+
return null;
|
|
126
|
+
const parameter = firstBoundReceiverParameter(params);
|
|
127
|
+
if (parameter === null)
|
|
95
128
|
return null;
|
|
129
|
+
const name = firstParameterName(parameter);
|
|
96
130
|
const className = classDefinitionName(enclosingClass);
|
|
97
|
-
if (className === null)
|
|
131
|
+
if (name === null || className === null)
|
|
98
132
|
return null;
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
return { kind: 'class', parameter, name, className };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a `@type-binding.self` (instance method) or `@type-binding.cls`
|
|
137
|
+
* (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
|
|
138
|
+
* method, is `@staticmethod`, or has no parameters.
|
|
139
|
+
*
|
|
140
|
+
* The caller is responsible for guaranteeing `fnNode.type ===
|
|
141
|
+
* 'function_definition'`.
|
|
142
|
+
*/
|
|
143
|
+
export function synthesizeReceiverTypeBinding(fnNode) {
|
|
144
|
+
const receiver = classifyPythonBoundReceiver(fnNode) ?? classifyPythonExplicitNewReceiver(fnNode);
|
|
145
|
+
if (receiver === null)
|
|
101
146
|
return null;
|
|
102
147
|
// Receiver convention: instance methods get `self`, classmethods get `cls`.
|
|
103
|
-
//
|
|
104
|
-
|
|
148
|
+
// The capture tag records the descriptor kind; the variable may use any
|
|
149
|
+
// spelling.
|
|
150
|
+
if (receiver.kind === 'class') {
|
|
105
151
|
return {
|
|
106
|
-
'@type-binding.cls': nodeToCapture('@type-binding.cls',
|
|
107
|
-
'@type-binding.name': syntheticCapture('@type-binding.name',
|
|
108
|
-
'@type-binding.type': syntheticCapture('@type-binding.type',
|
|
152
|
+
'@type-binding.cls': nodeToCapture('@type-binding.cls', receiver.parameter),
|
|
153
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', receiver.parameter, receiver.name),
|
|
154
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', receiver.parameter, receiver.className),
|
|
109
155
|
};
|
|
110
156
|
}
|
|
111
157
|
return {
|
|
112
|
-
'@type-binding.self': nodeToCapture('@type-binding.self',
|
|
113
|
-
'@type-binding.name': syntheticCapture('@type-binding.name',
|
|
114
|
-
'@type-binding.type': syntheticCapture('@type-binding.type',
|
|
158
|
+
'@type-binding.self': nodeToCapture('@type-binding.self', receiver.parameter),
|
|
159
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', receiver.parameter, receiver.name),
|
|
160
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', receiver.parameter, receiver.className),
|
|
115
161
|
};
|
|
116
162
|
}
|
|
117
163
|
/**
|
|
@@ -4,13 +4,27 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The provider is a thin wiring object — Python's specific bits
|
|
6
6
|
* (super recognizer, LEGB merge precedence, Python's relative-import
|
|
7
|
-
* resolver,
|
|
7
|
+
* resolver, C3 method resolution) plug into `runScopeResolution`.
|
|
8
8
|
*
|
|
9
9
|
* Migration reference: when bringing up the next language
|
|
10
10
|
* (TypeScript / Java / Kotlin / Ruby), copy this file's structure —
|
|
11
11
|
* implement the 6 required `ScopeResolver` fields, optionally toggle
|
|
12
12
|
* the 2 booleans, and register in `scope-resolution/pipeline/registry.ts`.
|
|
13
13
|
*/
|
|
14
|
-
import type {
|
|
14
|
+
import type { ReferenceSite, SymbolDefinition, TypeRef } from '../../../../_shared/index.js';
|
|
15
|
+
import type { ArityVerdict, ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
|
16
|
+
/**
|
|
17
|
+
* Python subtype dispatch is deliberately limited to instance receiver facts.
|
|
18
|
+
* Private names are class-mangled and cannot be matched by their source
|
|
19
|
+
* spelling across an eventual subtype. Argument-shape proof is candidate-level
|
|
20
|
+
* because it needs both the call-site and target-method capture facts.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pythonMissingReceiverSubtypeDecision(typeRef: TypeRef, context: {
|
|
23
|
+
readonly receiverBindingIsStatic: boolean | undefined;
|
|
24
|
+
readonly memberName: string;
|
|
25
|
+
readonly callArity: number | undefined;
|
|
26
|
+
}): boolean | 'suppress';
|
|
27
|
+
/** Additive compatibility proof for Python's missing-member subtype candidates. */
|
|
28
|
+
export declare function pythonMissingReceiverSubtypeCandidateCompatibility(callerFilePath: string, callsite: Pick<ReferenceSite, 'atRange'>, candidate: SymbolDefinition): ArityVerdict;
|
|
15
29
|
declare const pythonScopeResolver: ScopeResolver;
|
|
16
30
|
export { pythonScopeResolver };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The provider is a thin wiring object — Python's specific bits
|
|
6
6
|
* (super recognizer, LEGB merge precedence, Python's relative-import
|
|
7
|
-
* resolver,
|
|
7
|
+
* resolver, C3 method resolution) plug into `runScopeResolution`.
|
|
8
8
|
*
|
|
9
9
|
* Migration reference: when bringing up the next language
|
|
10
10
|
* (TypeScript / Java / Kotlin / Ruby), copy this file's structure —
|
|
@@ -12,11 +12,40 @@
|
|
|
12
12
|
* the 2 booleans, and register in `scope-resolution/pipeline/registry.ts`.
|
|
13
13
|
*/
|
|
14
14
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
15
|
-
import { buildMro,
|
|
15
|
+
import { buildMro, c3LinearizeStrategy } from '../../scope-resolution/passes/mro.js';
|
|
16
16
|
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
|
17
17
|
import { indexOnlyElementType } from '../../type-extractors/shared.js';
|
|
18
18
|
import { pythonProvider } from '../python.js';
|
|
19
19
|
import { isPythonImportedModule, pythonNamespaceReceiverPaths, pythonArityCompatibility, pythonMergeBindings, resolvePythonImportTarget, } from './index.js';
|
|
20
|
+
import { applyPythonSubtypeDispatchSideChannel, pythonSubtypeCallPositionalCount, pythonSubtypePositionalCapacity, } from './subtype-dispatch.js';
|
|
21
|
+
/**
|
|
22
|
+
* Python subtype dispatch is deliberately limited to instance receiver facts.
|
|
23
|
+
* Private names are class-mangled and cannot be matched by their source
|
|
24
|
+
* spelling across an eventual subtype. Argument-shape proof is candidate-level
|
|
25
|
+
* because it needs both the call-site and target-method capture facts.
|
|
26
|
+
*/
|
|
27
|
+
export function pythonMissingReceiverSubtypeDecision(typeRef, context) {
|
|
28
|
+
if (typeRef.source !== 'self' || context.receiverBindingIsStatic !== false)
|
|
29
|
+
return false;
|
|
30
|
+
const isPrivateName = context.memberName.startsWith('__') && !context.memberName.endsWith('__');
|
|
31
|
+
if (isPrivateName)
|
|
32
|
+
return 'suppress';
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** Additive compatibility proof for Python's missing-member subtype candidates. */
|
|
36
|
+
export function pythonMissingReceiverSubtypeCandidateCompatibility(callerFilePath, callsite, candidate) {
|
|
37
|
+
const positionalCount = pythonSubtypeCallPositionalCount(callerFilePath, callsite.atRange);
|
|
38
|
+
if (positionalCount === undefined)
|
|
39
|
+
return 'unknown';
|
|
40
|
+
const capacity = pythonSubtypePositionalCapacity(candidate);
|
|
41
|
+
const minimum = candidate.requiredParameterCount;
|
|
42
|
+
const maximum = candidate.parameterCount;
|
|
43
|
+
if (capacity === undefined || minimum === undefined || maximum === undefined)
|
|
44
|
+
return 'unknown';
|
|
45
|
+
if (positionalCount < minimum || positionalCount > maximum)
|
|
46
|
+
return 'incompatible';
|
|
47
|
+
return positionalCount <= capacity ? 'compatible' : 'incompatible';
|
|
48
|
+
}
|
|
20
49
|
const pythonScopeResolver = {
|
|
21
50
|
// A free call naming a class constructs it: `Service(db).do_work()` (#2708).
|
|
22
51
|
constructionSyntax: { bare: true },
|
|
@@ -55,9 +84,21 @@ const pythonScopeResolver = {
|
|
|
55
84
|
// Wrapper kept to honor both contracts without altering the legacy
|
|
56
85
|
// shape that LanguageProvider.arityCompatibility consumes.
|
|
57
86
|
arityCompatibility: (callsite, def) => pythonArityCompatibility(def, callsite),
|
|
58
|
-
buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup,
|
|
87
|
+
buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, c3LinearizeStrategy),
|
|
59
88
|
populateOwners: (parsed) => populateClassOwnedMembers(parsed),
|
|
89
|
+
applyCaptureSideChannel: applyPythonSubtypeDispatchSideChannel,
|
|
60
90
|
isSuperReceiver: (text) => /^super\s*\(/.test(text),
|
|
91
|
+
// A mixin may call a method supplied only by its eventual concrete class.
|
|
92
|
+
// Resolve the callable that DEFINED the binding, rather than the innermost
|
|
93
|
+
// caller, so a class receiver inherited by a closure cannot masquerade as
|
|
94
|
+
// instance dispatch. The helper also suppresses Python shapes whose exact
|
|
95
|
+
// target cannot be represented by the existing call-site facts.
|
|
96
|
+
resolveMissingReceiverMembersFromSubtypes: pythonMissingReceiverSubtypeDecision,
|
|
97
|
+
missingReceiverSubtypeCandidateCompatibility: (callsite, candidate, context) => pythonMissingReceiverSubtypeCandidateCompatibility(context.callerFilePath, callsite, candidate),
|
|
98
|
+
// Python permits both @staticmethod and @classmethod access through an
|
|
99
|
+
// instance. The graph's generic `isStatic` bit therefore does not mean
|
|
100
|
+
// "unreachable by instance dispatch" for this provider.
|
|
101
|
+
isStaticOnly: () => false,
|
|
61
102
|
// Subscript route only — Python spells collection views as method calls
|
|
62
103
|
// (`.values()`), which the compound resolver's call branch already handles.
|
|
63
104
|
//
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ParsedFile, SymbolDefinition } from '../../../../_shared/index.js';
|
|
2
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
3
|
+
type CallShapeTuple = readonly [line: number, column: number, positionalCount: number];
|
|
4
|
+
type CapacityTuple = readonly [line: number, column: number, capacity: number];
|
|
5
|
+
type LineMapper = (line: number) => number;
|
|
6
|
+
/**
|
|
7
|
+
* Python-private capture facts for conservative missing-member subtype dispatch.
|
|
8
|
+
* They stay opaque on `ParsedFile.captureSideChannel`; the public reference and
|
|
9
|
+
* definition schemas intentionally do not gain Python argument-binding fields.
|
|
10
|
+
*/
|
|
11
|
+
export interface PythonSubtypeDispatchSideChannel {
|
|
12
|
+
readonly kind: 'python-subtype-dispatch';
|
|
13
|
+
readonly simplePositionalCalls: readonly CallShapeTuple[];
|
|
14
|
+
readonly positionalCapacities: readonly CapacityTuple[];
|
|
15
|
+
}
|
|
16
|
+
/** Reset one file before a fresh capture or a worker snapshot restore. */
|
|
17
|
+
export declare function beginPythonSubtypeDispatchCapture(filePath: string): void;
|
|
18
|
+
/** Record calls whose arguments are all ordinary positional expressions. */
|
|
19
|
+
export declare function recordPythonSimplePositionalCall(filePath: string, callNode: SyntaxNode, positionalCount: number, mapLine?: LineMapper): void;
|
|
20
|
+
/** Record a method's exact fixed positional capacity when the AST proves it. */
|
|
21
|
+
export declare function recordPythonSubtypeMethodShape(filePath: string, fnNode: SyntaxNode, mapLine?: LineMapper): void;
|
|
22
|
+
/** Snapshot one worker file into structured-clone-safe plain data. */
|
|
23
|
+
export declare function collectPythonSubtypeDispatchSideChannel(filePath: string): PythonSubtypeDispatchSideChannel | undefined;
|
|
24
|
+
/** Restore worker/cache facts without reparsing source on the main thread. */
|
|
25
|
+
export declare function applyPythonSubtypeDispatchSideChannel(parsed: ParsedFile): void;
|
|
26
|
+
export declare function pythonSubtypeCallPositionalCount(filePath: string, range: {
|
|
27
|
+
readonly startLine: number;
|
|
28
|
+
readonly startCol: number;
|
|
29
|
+
}): number | undefined;
|
|
30
|
+
export declare function pythonSubtypePositionalCapacity(candidate: SymbolDefinition): number | undefined;
|
|
31
|
+
export {};
|