mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
fix(python): attribute calls follow the type the class body, a factory or a base gives the attribute (#1926)
Python calls through an attribute reach the method the code gives that attribute: `self.sink.stop()` follows `self.sink = Sink()`, a `sink: Sink` annotation, a typed `__init__` parameter, a factory's return type or what every `return` in it builds, and a base class's `__init__` in another file. Calls straight on a factory's result (`Client.from_env().send()`) resolve the same way, and a sync re-opens these answers when the factory or base they were read from changes. The native Python extractor now keeps a call's receiver text as the wasm one does, so `self.data.append(1)` is never a bare `append` that matches an unrelated project function. This includes all of PR 1704. Reworked from evidence-or-silence, which dropped about 3,000 correct Django links (`self.client.get()`, `self.engine.render_to_string()`). The reader now has three answers. A project class that has the method: that method. Evidence that rules project methods out: no edge, for a builtin, a class from outside the project built in a method or named by an annotation, an attribute only an outside base can give (Django's `TestCase` in an app), or a class without the method whose bases are all known. Nothing typed: the call is guessed by the name of its last link, read as written, as any receiver is, but only for an owner whose name holds the link's head noun (`self.session` for `SessionBase`, not `database.group_shopping_lists` for `GroupRepositoryAIProvider`) and never as the one method of a name; the guess records the files the reader looked at, so a sync re-opens it too. A class-body field an outside class builds (`author = models.ForeignKey(Author)`) may be a descriptor, so it is no evidence. A chain through an imported module (`os.path.exists()`) is the module's. In the shared scoring, a receiver that spells an owner's whole name (`client` for `Client`) outscores one that only shares its word (`RedisCacheClient`). A call through an imported value's attribute chain (`app.jinja_env.filters.keys()`) no longer links to the value's `jinja_env`. A/B against main with fresh indexes, every changed edge triaged, sites matched by position since the extractor now names these calls by their whole chain. Django: 3,317 calls linked that were not (`self.client.force_login()` to `ClientMixin`, `User.objects.create_user()` to `UserManager`, `self.stdout.write()` to `OutputWrapper`), 127 moved, nearly all to a more precise target (`self.site.register()` to `AdminSite.register` rather than the `register` decorator), and 134 removed: main's bare-name links on builtin values (`value.lower()` to the template filter, `.encode()` to a test password hasher) and calls on expressions such as `(q1 | q2).order_by()`, which now go unresolved as they do in the wasm engine. flask: 32 linked, 4 moved to `JSONProvider`. fastapi: 58 linked. mealie: 740 linked. Guesses still wrong, as main makes them for a plain receiver of the same name: fastapi's `request.headers.get()` to a telemetry `_HeadersGetter`, and about 20 of mealie's repository attributes to a controller. Go, C#, Ruby, Swift, Kotlin, Java and TS repositories are byte-identical, except excalidraw's 5 `props.onChange()` calls, which move between two unrelated `Props` interfaces. Indexing CPU: Django +7%, mealie +5%, fastapi flat; wall time unchanged within noise. Full suite on the merged tree: 7,999 passed, 0 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4286083a84
commit
642c7bb3a5
@@ -731,6 +731,30 @@ impl<'t> Walker<'t> {
|
||||
// TreeSitterExtractor.extractCall.
|
||||
let Some(inner) = self.plain_inner_callee(r) else { return };
|
||||
callee_name = format!("{inner}().{method_name}");
|
||||
} else if let Some(r) = receiver {
|
||||
// Any receiver shape the arms above do not claim — attribute
|
||||
// chain (`self.data.append`) and subscript (`d[k].append`).
|
||||
// The call-chain shape (`rows.setdefault(k, []).append(x)`)
|
||||
// is taken by the `<inner>().<method>` arm above (#1748),
|
||||
// which is matched first.
|
||||
// These used to fall through to a BARE method_name. A bare name
|
||||
// matching a common list/dict/str method (`append`, `get`,
|
||||
// `update`, ...) exact-matches an unrelated top-level project
|
||||
// function sharing that name, fabricating a call edge (#66,
|
||||
// same class as #1230/#1276). Keep the receiver's source text
|
||||
// as a qualifier so the ref can only resolve through
|
||||
// import/module-member resolution, never the bare-name
|
||||
// fallback; an unresolvable qualifier is a silent miss, never
|
||||
// a wrong edge. Mirrors TreeSitterExtractor.extractCall's
|
||||
// python branch — the whitespace collapse is JS `\s`
|
||||
// semantics, not `char::is_whitespace`, so both arms emit the
|
||||
// same bytes for the parity sweep.
|
||||
let receiver_text = collapse_js_whitespace(self.text(r));
|
||||
callee_name = if receiver_text.is_empty() {
|
||||
method_name.to_string()
|
||||
} else {
|
||||
format!("{receiver_text}.{method_name}")
|
||||
};
|
||||
} else {
|
||||
callee_name = method_name.to_string();
|
||||
}
|
||||
@@ -1105,6 +1129,35 @@ fn is_stoplisted(name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// `s.replace(/\s+/g, ' ').trim()` with JavaScript's `\s` class, so a receiver
|
||||
/// qualifier is byte-identical to the one TreeSitterExtractor emits. Rust's
|
||||
/// `char::is_whitespace` is NOT the same set: it counts U+0085 (NEL), which JS
|
||||
/// `\s` does not, and omits U+FEFF (ZWNBSP), which JS `\s` includes.
|
||||
fn collapse_js_whitespace(s: &str) -> String {
|
||||
fn is_js_space(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\t' | '\n' | '\u{b}' | '\u{c}' | '\r' | ' ' | '\u{a0}' | '\u{feff}'
|
||||
| '\u{2028}' | '\u{2029}'
|
||||
| '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{202f}' | '\u{205f}' | '\u{3000}'
|
||||
)
|
||||
}
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut in_space = false;
|
||||
for c in s.chars() {
|
||||
if is_js_space(c) {
|
||||
in_space = true;
|
||||
} else {
|
||||
if in_space && !out.is_empty() {
|
||||
out.push(' ');
|
||||
}
|
||||
in_space = false;
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// LITERAL_RECEIVER_TYPES membership (shared table; python names among them).
|
||||
fn is_literal_receiver(kind: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
Reference in New Issue
Block a user