Repository navigation
fix(python): calls like reg.get() on a typed local or parameter reach the class's method - #2821
Merged
Merged
Conversation
… the class's method In Python, a call of a method named like a builtin dict, list, string or file method (get, update, items, pop, close and the rest of the builtin-method list) was dropped by the builtin filter before any strategy could read its receiver. Only a receiver whose capitalized name is a project class, or an imported project module, got past it. So `reg = SystemPackRegistry(); reg.get(k)`, `registry = load_registry()` with `def load_registry() -> SystemPackRegistry`, and a parameter `reg: SystemPackRegistry` gave no edge, while `self.reg.get()` linked through the attribute reader of PR #1926. A call the filter drops now gets one way back, by evidence only: matchPythonTypedLocalCall. It reads the def that binds the receiver where the call reads it (the calling def, or one it is nested in). It types the name only when every binding there gives it the same project class: - a parameter gives what its annotation names ("T", Optional[T], T | None); - an assignment gives its own annotation, or what its whole value makes: a constructor, or a factory typed by its return annotation or by its returns (the attribute reader's pythonValueEvidence and pythonFactoryType); - `x = None` gives nothing either way; - each class is the one the file's imports name (pythonClassNamed), so a class imported from outside the project is none of the project's; - the method must be the class's own or a declared base's. Everything else stays dropped: any other binding of the name (a loop, with, except or comprehension target, a tuple or augmented assignment, a walrus, an import, del, a nested def or class of the name, a nonlocal write from a nested def), a value that names no class ({}, json.load(p), Foo().data(), a union), a rebound callee, a module-level name. These are the shapes the reporter's follow-up found making wrong edges with a looser reader. Like an attribute call, the edge records the other files its answer read (typeFrom), so a sync that edits the factory or a base re-opens it. Validation: - New __tests__/python-typed-builtin-method-calls.test.ts, 20 tests: - the issue's three calls; annotated, quoted and Optional parameters; items, pop and close; a module-qualified constructor; typed, untyped and awaited factories; a None placeholder; a local annotation; a closure; typeFrom; two same-named classes; an inherited method; - the reporter's trap shapes, which stay unlinked; - three sync cases, each matching a fresh index. With main's source the 13 positive tests fail and the 7 trap tests pass. - A/B against main 3f58e34. Both arms were built from their own sources with the kernel. Rebased onto e32021f (PR #2820, outside Python), the fix arm's graphs are byte-identical to before the rebase: - requests: +15 edges; - sqlalchemy: +52 edges; - django's 2,938 Python files: +73 edges; - 0 removed and no other change in any of them; - express and gin (controls): byte-identical. All 140 added edges were read at their source: each is a constructed local, a factory-typed local or an annotated parameter calling its own class's method, or one a declared base has. The issue's fixtures link all three calls, and the verification kit's nine trap call sites stay unresolved, kernel and wasm alike. - Cost: a CPU profile puts the new reader at about 1.0 s on django and 1.2 s on sqlalchemy, around 1% of index CPU. Index CPU medians of 3 interleaved runs, main vs fix: sqlalchemy 98.6 s vs 98.5 s, django 97.9 s vs 99.1 s (single runs vary by several seconds on this shared machine). - kernel-parity.mjs: 295/295 Python files at parity. Fixes #2583 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
Closed
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 29, 2026
…ons it exposed (#582) * fix(resolve): port upstream's lone same-file match confidence (cde4cd7) A lone same-file definition chosen by findBestMatch scored by directory proximity (0.7/0.4), losing to a framework's naming guess in another file. is_decisive_same_file_match scores it 0.9 when it is the only same-file candidate, is not the caller itself, and, as a member, belongs to an owner the caller is inside. Ports upstream cde4cd7 (colbymchenry#1938). same-file-match-confidence: 7/7. * chore(resolve): dump the kernel profile at close(), not only at drop The resolver is not dropped before process exit on the CLI index path, so CODEGRAPH_KERNEL_PROF=1 accumulated timings that never printed. close() is the deterministic teardown point. * test(ts): assert the fork's native error-tree path for export type * Upstream's kernel defers a file whose parse tree has errors, so the test expected tryKernelExtract to return null. The fork's tsjs walker extracts the recovered tree natively (cba9256) and keeps the re-export signature as written — assert that contract instead. * fix(resolve): port upstream's indexed import-path spelling (8651990) An alias, workspace or direct-path join kept a specifier's lexical spelling, so `@/widgets/Chart/` probed `Chart//index.tsx` and `src/widgets/./Chart` probed `./` segments no files row is keyed by. probe_extensions now spells each candidate as the index keys it for the ES-module family; other languages keep the path as found. Ports upstream 8651990 (colbymchenry#2792). import-path-spelling: 5/5. * fix(resolve): port upstream's Dart library-privacy rule (f7820b5) A Dart `_name` is private to its library — the library file and every file its `part` directives name — not to its file. private_is_file_local drops `dart`; is_visible_across_files gets the dart arm (a private candidate is seen only from a file of its own library); and the exact-name pre-pick filter declines a private candidate another library declares so the caller's own library's declaration wins. Ports upstream f7820b5 (colbymchenry#2832). dart-part-privacy: 10/12 — the two remaining failures resolve a typed receiver's private method through resolve_method_on_type's member path, whose visibility gate still needs tracing (upstream's dart arm at name-matcher.ts ~13147 / preferOwnDartLibrary is the next suspect). * fix(resolve): vet every strategy's pick through is_visible_across_files finish() applied name_post_guard but not the file/library-locality veto resolveOne's nameResult gets at index.ts:1867, so typed-receiver picks (instance-method) linked private members of another library. With the dart arm in place this completes f7820b5: dart-part-privacy 12/12. * fix(library): indexFiles keeps other writers out until its files are stored The file lock was released as soon as indexing started — the promise returned unawaited, the try ended, the finally let the lock go. Awaited, the lock now holds until the files are stored, as indexAll and sync hold it (upstream 865ddf4 / colbymchenry#2845). * fix(resolve): let a framework own a reference past the name strategies settleKernelOutcome merged kernel candidates beside the frameworks', so a project namesake could answer a call Nuxt's navigateTo owns (upstream 58eb57a / colbymchenry#2826). With ownsReference set, the frameworks' best answer wins or the ref stays unresolved. * test(vue): assert the fork contract for tsx blocks — grammar always compiled in Upstream's test asserted wasm-preload sets (9ee6718 / colbymchenry#2824); the fork has no wasm loader, so a tsx <script> can never lack its grammar. The block-mapping half was already fixed in sfcScriptLanguage. * fix(resolve): finish() visibility guard on the final node, name picks only Two corrections to the finish() is_visible_across_files port: - Run it on the node after supertype/overload/alias retargets, the way upstream vets the target matchReference reports — a pre-forward object property or import alias is not the node upstream inspects. - Vet only name-matcher picks: upstream's guard sits inside the matchReference arm, so import, file-path and framework results never reach it. An import pick's alias forward can legitimately land on an unexported module member (export const api = { run: right }). Also ports the Rust arm upstream added for typed receivers: a method a trait declares carries no pub of its own — reached through a typed receiver it is the trait's (rust_trait_of + rust_typed_refs, marking rust_written_call exits). Fixes the rust-* regressions the guard exposed (rust-typed-receivers, rust-receiver-chains, rust-generic-bound-paths, rust-method-value-types, rust-self-field-types, rust-wrapped-self-calls) while keeping dart-part-privacy 12/12. Remaining exposed failures are import-binding port debt, not guard bugs: items$.getState().inc (import-type-modifier) and styled(Disclosure) (function-ref-project-imports) resolve upstream through the import path the kernel does not fully carry. * feat(kernel): port Go value-type inference, indexed/chained/package-value call arms Port goReceiverChainType/goElementOf/goPackageValueType semantics tree-natively into the kernel: package-level var/const typing, indexed and chained receiver calls, and declared-type terminal vetoes for literals/outside types. Bound-receiver misses are terminal unless the receiver is an asserted local the index cannot type (upstream strategy-3 passthrough). TS gateTargetKind gains the missing upstream arms (import self-edge, Go extends qualification, dot-imported supertypes) and goBareTypeDir for type-position targets. * fix(kernel): resolve Go package-value calls at typed-receiver confidence Upstream's matchGoPackageValueCall resolves at 0.9; the arm inherited go_method_in_package's 0.85. * feat(kernel): Go element/field/call-result expression typing goElementOf (named slice/map types resolve through their declarations), goFieldTypeOf (struct field written types), selector-expression and call-expression result arms in go_expr_written. Param/decl locals the declaration reader misses fall back to infer_local_receiver_type. * feat(kernel): Go chained/generic receivers, range elements, unread-file methods * feat(kernel): Go build-constraint twins, external test packages, conversion refs (colbymchenry#2581) Port upstream 800e582's three Go semantics into the kernel: - A `pkg.Type(…)` conversion resolves its type through the import member arm — the kind filter now accepts Go exported member kinds (struct/interface/type_alias/variable/constant), which the TS edge-kind rewrite then turns into a `references` edge. - `go_name_visible` is clause-aware: files sharing a directory but declaring different package clauses (`a` vs `a_test`) see each other's bare top-level names only through an import — qualified, or bare after a dot import and exported. - New `go_build.rs` ports `goFileConstraint`/`preferGoBuildTwin`: `//go:build` and `// +build` expressions plus `_GOOS`/`_GOARCH` filename suffixes evaluate against the ordered GO_BUILD_TARGETS; a resolved target with constrained twins retargets to the twin the reference's own file builds with, and `go_pick_twin` collapses multi-candidate package members the import arm couldn't pick. Also fixes the fork's `go-type-visibility.ts`: module-root packages spell their dir `''` (matching `getGoPackageDir` and kernel `pos_dirname`), so `goTypePositionTarget` retargets a namesake function to the module-root type. * feat(kernel): VB.NET namespace receivers, overload pick, head members (colbymchenry#2762) Port upstream dbb6aa9: VbReceiver gains a namespace arm (vb_known_namespaces/vb_namespace_around/vb_namespace_named/ vb_types_in_namespace/vb_module_member_in) so reads like Billing.Invoice.MaxLines and Acme.Billing.Invoice.MaxLines link the type or Module member the namespace declares; vb_takes_no_arguments + vb_runnable_overload pick the overload VB.NET runs when a method is named without parentheses (Me.Load, Optional/ParamArray, a ctor's Me.New never resolves to itself); vb_head_member + vb_with_head link a bare member before the dot (Button1.Text) when no local or parameter shadows it, gated by foreign-Module and outside-namesake checks. vb_receiver_type's member search is extracted as vb_member_around. * feat(kernel): workspace package entries, path deps, manifest-edit sync (colbymchenry#2502, colbymchenry#2456) Port e8ddc4e's declared-entry import resolution into the kernel and its sync-side invalidation into codegraph.ts. Kernel (napi config grows entriesByDir + scopedByName): - resolve_workspace_import takes from_file; workspace_import_targets tries declared entries first — exports (exact, wildcard, fallback arrays, nested conditions), then source/module/main/types/typings in order — then an indexed direct probe, then emitted→source mapping, then the direct probe as last resort. Path deps (file:/link:/portal:) resolve scoped by the importer's nearest manifest, before global names; longest package name wins. - imports.rs gains longest_package_name/match_workspace_package/ declared_workspace_entries/indexed_target/built_source_of plus WorkspaceTargets/exported_targets/built_sources_of; prefilter passes the importing file through. TS sync side (upstream index.ts port): - resolver: refreshWorkspacePackages/getWorkspaceFingerprint/ workspacePackageDirOf; a moved map closes the kernel since the config is construction-time. - codegraph.ts: js_workspace_fingerprint metadata stamped at index and re-checked on full reconcile; a moved fingerprint re-opens every ESM-language ref for the orphan sweep. reopenReferencesThroughAddedFiles gets packageDirOf so a late-appearing declared entry re-links importers. - loader.ts KernelWorkspaceIn + serialization gain the two new maps. workspace-package-entries: 26/26. Regression sweep: 146/147, the one failure a pre-existing tracked bucket (7180fd0 colbymchenry#2788). * feat(kernel): a call on a this-field element resolves on the element's type Port 449eefa's resolver half: `this.items[0].save()` arrives as `this.items[].save` from the extractors (already ported) and now binds the element type the field's declaration writes instead of bare-name matching any project method. - method_call_shape: elementOf arm `^this\.(#?[\w$]+)\[\]\.([\w$]+)$` → match_ts_this_field_call(field, method, r, element=true) — exclusive, like the plain `this.field.m` arm. - TsFieldDecl gains element; ts_field_element_type reads it off the written type (`Item[]`, `Array<Item>`, `ReadonlyArray<Item>`, `Record<string, Item>`) and rejects unions, arrays of arrays and non-plain elements; ts_field_element adds tsFieldRead's multi-line continuation guard (`| Other[]` on the lines below). - ts_field_held filters element reads through ts_class_type_parameter — `class Store<Item> { items: Item[] }` holds no project `Item`. - ts_class_type_parameter ported upstream: `class|interface|type` keyword anchor, 24-line multi-line head, returns true when the list's `>` is not found (drop the type rather than take a parameter for a class). ts-this-field-element 11/11; expression-receiver-calls + ts-array-annotated-receivers green; receiver sweep shows only tracked debt (08478ee colbymchenry#2811, 013b91c colbymchenry#2825); golden dumps unchanged. * feat(kernel): calls on a typed local or parameter reach the builtin-named method (colbymchenry#2821) A python call `reg.get()` on a builtin-named method was dropped by the builtin filter before any strategy could read the receiver. Port matchPythonTypedLocalCall (colbymchenry#2583) into python_attribute_verdict: a plain local or parameter receiver settles only through scope evidence — `python_local_class` agrees a binding def's parameter annotation (quoted, Optional[T], Union[..., None], T | None, read off the raw def header) or its assignments (ctor/factory/None; any foreign binding shape, rebinding or contested evidence refuses). Members come from `python_strict_members` — the class's own or a declared base's, resolved through the file's imports, so a base from outside the project holds none of its methods and the extends edges never substitute a same-name class. Sync side: FAILED_PY_ATTR_REF widened to the one-dot `x.y` shape. The fork's delta-narrowed retry keys (perf/sync-delta-retry) never list an unchanged name like `get`, so a signature-only edit to the factory a failed typed-local call reads its type from gave no retry key; the attr-retry lane kernel-probes candidates and retries only resolvable ones, which also lands the edge's typeFrom stamp for future edits. * feat(kernel): a call on an unqualified call result resolves on its return type (colbymchenry#2822) `repository().pending()` — and `val repo = repository()` locals — type the receiver by the inner call's declared return, so `pending` is the method the returned project type declares, never a same-named method elsewhere. A type from outside the project leaves the name strategies untouched, and an extension the bound-member walk can't see keeps the old name match. The extractor's return_type keeps only a user_type's first identifier (`Request`), so `kotlin_scoped_hit` retypes the callee through its signature (`(): Request.Builder`) and writes a lone nested name through its declaring outer (`Builder` → `Shop.Builder`). Two pre-existing filters were misaligned with the kotlinCall/kotlinBare split this relies on: member-scope reachability vetted receiver-written names (`text().reverse`) as if receiver-less, and a Chain-shape call with a non-project receiver type (`StringBuilder`) vetoed every name match — upstream only constrains standard-library names there. Both now match upstream's isReceiverLessKotlinCall / kotlinStdChain boundary. Also: python_local_class returns Option<Answer> — a name no enclosing def binds (module-level, global) is upstream's `undefined`, not a refused local, which restores module-global `store = Store()` receivers the strict arm claimed and dropped. * feat(kernel): non-test refs resolve as if Jest manual mocks were absent (colbymchenry#2790) Port upstream e69546e's mockless context: a ref from non-test code resolves on a graph without the project's __mocks__/ files, while test code keeps the whole graph. resolve_ref sets a per-ref mockless flag; nodes_by_name, nodes_by_lower_name and nodes_by_qualified_name then drop __mocks__/ nodes (gated on the cached mock-name sets so unaffected names aren't re-collected). The TS callback-synthesis guards were already in place. Also fix ts_scoped_reference's type-ref tail: upstream importedTypeMember falls through (undefined) on a found-miss unless the specifier is outside the repository — a resolved package directory whose exports can't be read is not a refusal. The old gate refused references/extends refs bound to a no-entry workspace package, which upstream answers by name-match. * fix(sync): record files a JSX tag import reads through as synthesis inputs (colbymchenry#2563) collectSynthesisInputs only kept files whose own content matched mayFeedSynthesis, so a barrel or index that merely forwarded another module's component was never recorded as an input. When a later sync edited only that index, the JSX pass never reran and the tag kept its stale endpoint. Port upstream's importedTagModules: for each JSX-bearing file, map its capitalized tags to project imports and collect every file exportedComponent reads through, memoized per collection run. * feat(kernel): resolve Rust calls by the receiver's written type (colbymchenry#2831) Port upstream 8fdc8a1: a field chain or struct literal receiver's declared type decides the call — `vis.map.get` is HashMap's, `Xyz{}.to_val` is Xyz's — and a path call naming a type std or a dependency owns reaches only the project's impls of that type or the named trait, never a project namesake. - rust_expr_type types tuple-struct/enum-variant constructor calls - undeclared-type guard limited to plain named `field_expression` heads, exempting generics, Self, outside types, use-aliases and wrapper-own methods; derived-trait provided methods resolve through `#[derive]` - rust_path_call_reachable gates name-match on the path's outside type Rust sweep 28 files/206 tests green; kernel golden dumps clean; the 16 remaining full-suite failures are all tracked port debt. * feat(kernel): scope named class expressions and export them over CommonJS (colbymchenry#2825) A named class expression is a class bound inside its expression: index its members and supertypes, but resolve references written outside the expression on a graph where that class and its members are not — a same-named declaration or binding keeps its links, and `new Widget()` elsewhere links nothing. References written inside see the whole graph unless the name is another expression's, bound inside that one alone. `module.exports = class Router` / `exports.Store = class StoreImpl` are the module's value: the class stays reachable to every importer, and collect_cjs_export records the declared name as the export's local binding. Inheritance refs resolved through an import binding emit upstream's import confidence 0.9; the scoped arm's 0.95 stays for type-position references. * fix(kernel): a call or new never lands on an interface or a type alias (colbymchenry#2801) Nothing calls an interface or a type alias, or makes an instance of one. A calls/instantiates result that found a TypeScript type by name or through an import meant the value its file declares beside it — the function under `interface Ok` in a result module, the class merged with an interface — and a global the project only types, like `new Response()` or `new Worker()`, links nothing. * fix(kernel): an explicit operator call reads its own argument list tree-sitter-cpp parses `d.operator+=(c)` as a call on the receiver alone, with the operator's name stranded in an ERROR sibling (`function: (identifier) (ERROR ".operator+=") arguments: (c)`), so the callee never spells `operator+=` and cpp_call_argument_types found no call. Every overload then tied at 0 and the first — `operator+=(int)` — took a call that `operator+=(const Counter&)` fits. Accept a call whose error spells the operator name (the member-access dot is part of the error text), so the argument types are read and the class overload wins. `size_type` and `difference_type` count as numbers now, the container convention upstream uses (6bd217a): nlohmann/json's `p->operator[](0)` reaches the index overload, not the `T*` one. * fix(kernel): a typed-local receiver keeps the invocation-time binding The colbymchenry#2821 port (0b564c78) settled a builtin-named method call through pythonLocalClass alone, which refuses when a name is bound more than once. `store = A(); store = B(); store.get()` then resolved to nothing, though the fork's receiver-scope rule has always read the binding in effect at the call (B). Read that binding: among plain assignments at or before the call, the last one decides, and a bare annotation (`store: B`) does not rebind. python_receiver_uncertain still holds back a same-line replacement and a later replacement a closure's captured cell sees, and a later binding no longer poisons an earlier one when the call reads before it. A def's parameter keeps its annotation when nothing rebinds it. Three shapes the strict read owns stay with it: a binding that is not a plain assignment (a branch, loop, `with`, comprehension), a base from outside the project, and a future binding read from a lambda body. A receiver naming a class declared here (`File.write(f, "x")`) and a lambda parameter of the receiver's name keep the ordinary paths. * fix(resolve): vet only name picks through a module's unexported bindings 2ca95adb applied isVisibleAcrossFiles to every strategy's pick. Upstream guards just the name match (index.ts:1867), and the port's ESM leg vetoed picks that were never name guesses: a typed store action (`items$.getState().inc()`) and an imported component function value were both dropped as unexported module bindings. Keep every language's privacy rule for a structured pick, and skip the ESM export check for a pick a receiver's type or a function value made. * test(eval): record the json, flask, vite and svelte precision runs Every case held on each corpus (json 11 absent + 7 present, flask 2 present, vite 6 absent + 2 present, svelte 1 absent). * fix(kernel): close the review findings on the three fixes - C++ (P3): a comment inside the access (`b./* note */operator+=(a)`) left the comment in the error's text, so the operator name never matched. Compare the `operator_name` node the grammar still reads, and keep the trimmed-text form only when there is none. - Python (P2): a local read before its only assignment in the same def (`result = store.get(); store = A()`) is an UnboundLocalError, not the later value; refuse it, keeping the future-initialization exception for a nested def that captures the outer name. - Python (P2): two conditional bindings in different blocks leave no value in effect (`if flag: store = A() else: store = B()`, then `store.get()`); refuse. One conditional beside a `None` placeholder or a later plain assignment still reads, and a conflicting set whose binding in effect may not run stays unknown. - ESM (P2): a function value is import-justified only when the file's own import resolves to the target's file, so a bare name fallback can no longer take an unexported namesake from another module. A default-exported wrapper still reaches its inner function through the default import, and `export default function Name` counts as an exported name. Regression tests: commented operator access, a future local read, two class-binding branches, and an imported name the module does not export. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 30, 2026
* feat(kernel): port the remaining upstream resolution drift Ports the behaviours upstream added after the pinned 8e854d1: calls on browser host objects (colbymchenry#2907), enclosing-class member calls, Go receiver typing through parameters, func-typed fields and call results (colbymchenry#2543, colbymchenry#2581), library-method names on untyped receivers, and same-module copies. The port's own tests found five gaps, fixed here: - tree-sitter-cpp strands an explicit operator's name in an `ERROR` sibling (`d.operator+=(c)`), so the argument list was never read and the first overload won; the operator's `operator_name` node is matched instead, comments inside the access included. - a func-typed Go field (`get func() A`) was typed by its first token; the whole type is kept and the call takes the func's result. - Go result typing read a package's function even when a parameter of the package's name shadowed it. - a typed-local receiver kept only the strict scope read, so a rebinding (`store = A(); store = B()`) resolved to nothing; the invocation-time binding decides, with the colbymchenry#2821 refusals intact. - Python inherited members followed breadth-first edge order, so `Adapter(Left, Right)` with `Left(Root)` reached Right's method before Root's; the walk is depth-first along the header, and `*bases` expands its module-level tuple. `isVisibleAcrossFiles` guards name picks only, as upstream does; a typed store action or an imported component value is no unexported-module pick. JS/TS browser host globals are read as the browser's own unless the file binds the name. Golden dumps: `torture-multilang` re-baselined for one Java edge, whose confidence rises 0.4 -> 0.9 because `var x = Method()` now types the receiver; the target is unchanged. * fix(kernel): close the review findings - Browser host scan: the `typeof` lookbehind now starts on a character boundary (a non-ASCII byte could panic the slice), and a lexical binding of the name at the call keeps the name the file's own, which a `${…}` interpolation's blanked contents cannot hide. - Python inherited members: `inherited_through` reads Python's C3 order (each class's own member, not its subtree's) and `python_c3` linearizes `python_bases`, `*bases` included. The header-position sort in `python_mro` is reverted: it could match the subclass's own name first (`class Model(Mixin, parent.Model)`). - A func-typed Go field stops at `;` and a struct tag, so `get func() *A; other *B` is `func() *A`. - The generic-parameter exception is limited to `Recv`/`RecvMsg` on a binding that is still a parameter, so `s.Header()` does not enable name matching for a project `Resp.Get`. - `*bases` is read from the module-level tuple written before the class, not an earlier function's local. - python_types.rs: the conditional-binding guard's regex had a literal backspace instead of `\b`; the chosen conditional binding is now refused when the evidence conflicts. Tests: a non-ASCII receiver line, a template interpolation's `document` parameter, a same-line second Go field with a struct tag, a diamond C3 case, and `class Model(Mixin, parent.Model)`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In Python, a call of your own class's method named like a builtin dict, list, string or file method —
get,update,items,pop,closeand the rest of the builtin-method list — now links to that method when the function says what the receiver holds:All three calls from the issue link now. The shapes from the reporter's follow-up comment, where a looser receiver typing made wrong edges, still link nothing.
Root cause
isBuiltInOrExternal(src/resolution/index.ts) drops a dotted Python call whose method is inPYTHON_BUILT_IN_METHODSbefore any strategy runs. It keeps the call only when the capitalized receiver name is a project class (systemPackRegistry→SystemPackRegistry) or the receiver is an imported project module (#1681). A typed local or parameter with any other name never reachedmatchMethodCall.self.reg.get()already linked, because the attribute reader from #1926 runs on the attribute chain.Fix
A call the filter drops gets one way back, by evidence only.
matchPythonTypedLocalCall(src/resolution/name-matcher.ts) reads the def that binds the receiver where the call reads it: the calling def, or one it is nested in. It types the name only when every binding there gives it the same project class:"T",Optional[T],T | None);-> Tor by its returns. These are the attribute reader'spythonValueEvidenceandpythonFactoryType, soFoo().data()is no evidence ofFoo;x = Nonesays nothing either way;pythonClassNamed).from queue import Queuenames no project class, even when the project has aQueue, andfrom b import Handleris b'sHandler, not a's;Everything else leaves the receiver unknown and the call dropped, as before:
for,with,exceptor comprehension target, a tuple or augmented assignment, a walrus, an import,del, a nesteddeforclassof the name, or anonlocalwrite from a nested def;{},json.load(p),Foo().data()or a union;Store = load(); reg = Store());These are exactly the shapes in the reporter's 2026-10-15 comment. The edge records the other files its answer was read from (
typeFrom), as an attribute call's does, so a sync that edits the factory or a base re-opens it.Only calls the filter used to drop can change, and only Python ones.
Validation
Tests. New
__tests__/python-typed-builtin-method-calls.test.ts, 20 tests:Optionalparameters;items/pop/close; a module-qualified constructor; typed, untyped and awaited factories; aNoneplaceholder; a local annotation; a closure;typeFrom; two same-named classes; an inherited method;Queue/Session/Counter/StringIO, rebinding to{}andjson.load, branches, loops,with, comprehensions,Foo().data()/Foo().build(), a union, an unannotated parameter, a rebound class name, a rebound global, module-level code, and an outside base;Against main's source the 13 positive tests fail and the 7 trap tests pass; with the fix all 20 pass. The 29 Python-related suite files (572 tests) pass.
A/B against main
3f58e343. Both arms were built from their own sources, kernel included, indexed fresh with the default engine, dumped withscripts/dump-graph.mjsand diffed by edge site. After the rebase ontoe32021f6(#2820, Java/Kotlin/C#/Scala only), the fix arm's dumps are byte-identical to before the rebase.611c616269e49c9c.pyfiles only8e4e95da0dee2c520f09c3a9I read the source behind every added edge. Each one is a constructed local, a factory-typed local or an annotated parameter calling its own class's method, or one a declared base has. Examples:
jar = requests.cookies.RequestsCookieJar(); jar.get(…);record: ConnectionPoolEntry→record.close(), andsess = object_session(…)→Session.flush(through-> Optional[Session]);clause = WhereNode(…); clause.add(…)→tree.Node.add(inherited), andstream = LimitedStream(…); stream.readline().Django was indexed as a copy of its Python files, under the sweep's RAM rule for big repos; its other files take no part in Python resolution.
The issue's fixtures link all three calls on both engines. The verification kit's nine trap call sites stay unresolved on both, identical to main.
Cost. Index CPU, median of 3 interleaved runs, main vs fix: sqlalchemy 98.6 s vs 98.5 s, django 97.9 s vs 99.1 s. Single runs vary by several seconds on this shared machine, so I also took a CPU profile: it puts the new reader at about 1.0 s on django and 1.2 s on sqlalchemy, around 1% of index CPU.
Kernel parity.
scripts/kernel-parity.mjs --lang pythonover requests and sqlalchemy/lib: 295/295 files at parity. The change is resolution-only.Full suite on the pushed commit
0008dc90(CODEGRAPH_KERNEL_EXPECT=1, kernel built): 738 files (732 passed, 6 skipped), 8,852 tests: 8,770 passed, 0 failed, 82 skipped. No reruns were needed.Fixes #2583
Related
isBuiltInOrExternal.Left out
__init__.py, likefrom django.template import Context(defined indjango/template/context.py). The class lookup is the attribute reader'spythonClassNamed, which doesn't follow re-exports, soself.ctx.update()misses the same class today. Fixing it for both readers together is a separate change.reg: Optional[Registry] = None, is not read yet. The same annotation on a parameter is._svc = Registry()at module scope, then_svc.get()) stays dropped for builtin names; Python:_svc.delete()on a module-level instance gets no edge unless the method name is unique #2582 types those for other method names. Reusing its reader here added 6 correct sqlalchemy edges but about 2 s per index, because it re-reads whole class bodies on every call, so I left it out.getin Django), sync doesn't retry the call, and it links at the next full index. That is the existing per-name retry ceiling. In smaller projects the sync cases above converge.🤖 Generated with Claude Code