fix(resolution): reject Python module collection method guesses (#1652) (#1776)

Use same-file module variable initializers to filter builtin collection calls before class-name heuristics, and require a Python type node for the class escape. Preserve imported project module calls.

Add callers/callees regressions for dict, list, set, tuple, and frozenset, with a real instance control and same-name bindings across files.

Validation on Linux with Node 22.19.0: the new suite had 14 failures and one passing control on main at 8733c288; all 216 tests in the new suite, call-receiver-no-fabrication, and resolution now pass. TypeScript and copy-assets pass. Re-indexed /tmp/cg-1652-repro: callers get is empty and read_setting no longer calls a cache.py method.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 10:29:53 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 8733c2880f
commit 195888d71f
3 changed files with 124 additions and 4 deletions
+17 -4
View File
@@ -2104,8 +2104,22 @@ export class ReferenceResolver {
// project dependency, not `list.append` (#1681). Without this, the
// qualified ref never reaches resolveViaImport / resolvePythonModuleMember.
if (PYTHON_BUILT_IN_METHODS.has(method)) {
// A module-scope collection binding is stronger evidence than a
// coincidentally matching class name (#1652). Only use this file's
// binding: an unrelated module may reuse the receiver for a collection.
const isCollection = this.context.getNodesByName(receiver).some((node) =>
node.language === 'python' && node.filePath === ref.filePath &&
(node.kind === 'variable' || node.kind === 'constant') &&
node.qualifiedName === receiver &&
/^=\s*(?:[\[{]|(?:dict|list|set|tuple|frozenset)\s*\(|\(\s*\)|\([^()]*,)/.test(node.signature ?? '')
);
if (isCollection) return true;
const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1);
const isKnownClass = this.knownNames?.has(capitalized) ?? false;
const isKnownClass = this.context.getNodesByName(capitalized).some((node) =>
node.language === 'python' &&
(node.kind === 'class' || node.kind === 'struct' || node.kind === 'interface')
);
const isProjectModule =
!isKnownClass && this.isPythonProjectModule(ref, receiver);
if (!isKnownClass && !isProjectModule) {
@@ -2116,9 +2130,8 @@ export class ReferenceResolver {
// A bare name colliding with a builtin method (index, get, update, count…)
// is only a builtin when NOTHING in the codebase declares it. A declared
// symbol with that exact name — e.g. a Flask/FastAPI view `def index()` or
// `def get()` — is a real reference target. Mirrors the knownNames guard on
// the dotted branch above; without it, every handler named after a builtin
// method silently loses its route→handler edge.
// `def get()` — is a real reference target. Without this guard, every
// handler named after a builtin method silently loses its route→handler edge.
if (PYTHON_BUILT_IN_METHODS.has(name) && !this.knownNames?.has(name)) {
return true;
}