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.
Files changed (29) hide show
  1. package/README.md +4 -2
  2. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +5 -4
  3. package/dist/core/ingestion/languages/python/arity-metadata.js +5 -4
  4. package/dist/core/ingestion/languages/python/captures.js +32 -0
  5. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +15 -1
  6. package/dist/core/ingestion/languages/python/receiver-binding.js +87 -41
  7. package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -2
  8. package/dist/core/ingestion/languages/python/scope-resolver.js +44 -3
  9. package/dist/core/ingestion/languages/python/subtype-dispatch.d.ts +31 -0
  10. package/dist/core/ingestion/languages/python/subtype-dispatch.js +183 -0
  11. package/dist/core/ingestion/languages/python.js +3 -0
  12. package/dist/core/ingestion/method-extractors/configs/python.js +17 -13
  13. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +55 -10
  14. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +5 -4
  15. package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +6 -7
  16. package/dist/core/ingestion/scope-resolution/passes/mro.js +30 -7
  17. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +21 -2
  18. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +227 -2
  19. package/dist/core/ingestion/scope-resolution/pipeline/run.js +16 -4
  20. package/dist/mcp/local/local-backend.d.ts +21 -0
  21. package/dist/mcp/local/local-backend.js +187 -1
  22. package/dist/mcp/read-only-policy.js +2 -0
  23. package/dist/mcp/server.d.ts +1 -1
  24. package/dist/mcp/server.js +5 -1
  25. package/dist/mcp/tools.d.ts +7 -1
  26. package/dist/mcp/tools.js +97 -1
  27. package/dist/server/grep-params.js +4 -0
  28. package/dist/storage/parse-cache.js +11 -1
  29. 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 **17 tools** (15 per-repo + 2 group) automatically:
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`; omitting it queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
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
- * - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
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`) collapses `parameterCount` to
12
- * `undefined`, which `pythonArityCompatibility` then treats as
13
- * `'unknown'` — keeping the candidate in the registry's lookup set.
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
- * - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
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`) collapses `parameterCount` to
12
- * `undefined`, which `pythonArityCompatibility` then treats as
13
- * `'unknown'` — keeping the candidate in the registry's lookup set.
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 directly inside a class body" via a single static query.
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 directly inside a class body" via a single static query.
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 firstNamedParameter(parameters) {
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
- // Skip `*` / `/` markers.
52
- if (child.type === 'positional_separator' || child.type === 'keyword_separator')
52
+ if (child.type === 'comment')
53
53
  continue;
54
- return child;
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
- // first child holds the identifier / pattern.
63
- const ident = param.childForFieldName('name') ?? findIdentifierChild(param);
64
- return ident?.text ?? null;
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
- * Build a `@type-binding.self` (instance method) or `@type-binding.cls`
76
- * (`@classmethod`) match for `fnNode`, or `null` if `fnNode` is not a
77
- * method, is `@staticmethod`, or has no parameters.
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 synthesizeReceiverTypeBinding(fnNode) {
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
- // Skip @staticmethod-decorated methods (no implicit receiver).
87
- if (hasDecorator(fnNode, 'staticmethod'))
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 first = firstNamedParameter(params);
94
- if (first === null)
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
- const firstName = firstParameterName(first);
100
- if (firstName === null)
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
- // We trust the AST literal name (Python convention is strict in practice).
104
- if (isClassmethod) {
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', first),
107
- '@type-binding.name': syntheticCapture('@type-binding.name', first, firstName),
108
- '@type-binding.type': syntheticCapture('@type-binding.type', first, className),
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', first),
113
- '@type-binding.name': syntheticCapture('@type-binding.name', first, firstName),
114
- '@type-binding.type': syntheticCapture('@type-binding.type', first, className),
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, the simplified MRO walk) plug into `runScopeResolution`.
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 { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
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, the simplified MRO walk) plug into `runScopeResolution`.
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, defaultLinearize } from '../../scope-resolution/passes/mro.js';
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, defaultLinearize),
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 {};