fix(resolution): resolve module-qualified calls colliding with builtin methods (#1749)

isBuiltInOrExternal treated ledger.append as list.append unless the receiver
matched a known class, so real module exports never reached resolveViaImport.
Allow project-module receivers (verified via resolveImportPath) through while
keeping stdlib/PyPI silent. Completes #1681 after #1748 fixed the FP half.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 00:27:27 -05:00
committed by GitHub
co-authored by Colby McHenry
parent bb1d3093eb
commit edcd36e5f0
3 changed files with 102 additions and 3 deletions
+47 -3
View File
@@ -17,7 +17,7 @@ import {
ImportMapping,
} from './types';
import { isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher';
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos } from './import-resolver';
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos, resolveImportPath } from './import-resolver';
import { ResolverPool, minRefsForPool } from './resolver-pool';
import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
@@ -2002,6 +2002,42 @@ export class ReferenceResolver {
return this.frameworks.map((f) => f.name);
}
/**
* True when `receiver` is a local name bound by an import that resolves to a
* file IN THIS PROJECT — the only case where letting a python
* built-in-method name through the filter is safe (#1681).
*
* Asking only whether SOME import bound the local name is not enough: every
* import produces a mapping, stdlib and PyPI included, so that would also be
* true for `os`, `requests`, `np`. Opening the filter for them lets
* resolveViaImport find no project file, fall through to bare-name matching,
* and bind `os.remove(p)` to whatever project method happens to be named
* `remove` — reintroducing, through its own escape hatch, the fabricated-edge
* class this filter exists to prevent.
*
* Resolving the specifier is the same question resolveViaImport will ask
* next, so a receiver that passes here is one the qualified path can actually
* serve; anything else stays a silent miss rather than a wrong edge.
*/
private isPythonProjectModule(ref: UnresolvedRef, receiver: string): boolean {
for (const imp of this.context.getImportMappings(ref.filePath, ref.language)) {
if (imp.localName !== receiver) continue;
// `import pkg.mod` / `import pkg.mod as m` binds the module `source`
// names. `from pkg import mod` binds `pkg.mod`, and `from . import mod`
// binds `.mod` — join without doubling the dot that makes `.` mean the
// current package.
const specifier = imp.isNamespace
? imp.source
: imp.source.endsWith('.')
? `${imp.source}${imp.exportedName}`
: `${imp.source}.${imp.exportedName}`;
if (resolveImportPath(specifier, ref.filePath, ref.language!, this.context)) {
return true;
}
}
return false;
}
/**
* Check if reference is to a built-in or external symbol
*/
@@ -2050,10 +2086,18 @@ export class ReferenceResolver {
}
// Filter built-in methods on non-class receivers
// (e.g., items.append where items is a local list variable)
// But allow if the capitalized receiver matches a known codebase class
// But allow if the capitalized receiver matches a known codebase class,
// OR the receiver is itself an imported project module — a module can
// export a top-level function sharing a common collection-method name
// (`ledger.append`, `from . import ledger`), and that call is a real
// project dependency, not `list.append` (#1681). Without this, the
// qualified ref never reaches resolveViaImport / resolvePythonModuleMember.
if (PYTHON_BUILT_IN_METHODS.has(method)) {
const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1);
if (!this.knownNames?.has(capitalized)) {
const isKnownClass = this.knownNames?.has(capitalized) ?? false;
const isProjectModule =
!isKnownClass && this.isPythonProjectModule(ref, receiver);
if (!isKnownClass && !isProjectModule) {
return true;
}
}