mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
fix(python): a member value through an untyped receiver is a method only its owner names (#2218)
A lone project method now stands for an untyped receiver's member value only when the receiver is named after the method's class: netbox's `device=self.parent.device` model-field reads no longer reach a GraphQL filter's `device` method. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In Python, an attribute passed as a value through an object nothing types, like netbox's `device=self.parent.device`, is no longer linked to the project's only method of that name when the object isn't named after the method's class. Before, netbox's model-field reads went to a GraphQL filter's `device` method, and healthchecks' `check.last_ping` went to a notification transport's.
|
||||
- In Scala, a kind-projector placeholder in a type, like `Align[Either[A, *]]`, is no longer linked to an operator method named `*`. A symbolic name in a type, like cats' `F ~> G`, now resolves only to a type.
|
||||
- C# type names now follow namespaces. A type is in reach from its own namespace or one nested inside it, or through a `using` of its namespace. Also counted: a project-wide `global using`, the project's `<Using>` entries, the SDK's implicit usings, or an alias that names it. Before, Newtonsoft's `async Task` tests linked `Task` to a test class of that name in another namespace, and AutoMapper's DTOs linked to another namespace's `BaseEntity`.
|
||||
- Rust type and function names now follow Rust's scoping. A name from another file counts only when a `use` brings it in (or a `*` glob over its module). One imported from outside the project, like `use std::task::{Context, Poll}` or `use futures::Stream`, means that outside item. A path names its module: `io::Result` isn't an unrelated `Result` alias elsewhere, and `std::io::Error` is std's. On tokio, over 1,200 `io::Result` and 600 `Context` references had been landing on private project types.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* A member passed as a value through a receiver nothing types —
|
||||
* `device=self.parent.device`, `self.check.last_ping` — is only the
|
||||
* project's one method of that name when the receiver is named after its
|
||||
* owner: netbox's model-field reads bound to a GraphQL filter's `device`
|
||||
* method, healthchecks' `check.last_ping` to a transport's.
|
||||
*/
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-member-fnref-'));
|
||||
const files: Record<string, string> = {
|
||||
'app/filters.py': `class FHRPGroupAssignmentFilter:
|
||||
def device(self, queryset):
|
||||
return queryset
|
||||
`,
|
||||
'app/components.py': `def describe(parent):
|
||||
return "{interface}".format(interface=parent, device=parent.device)
|
||||
`,
|
||||
'app/recorder.py': `class HookRecorder:
|
||||
def finish_recording(self):
|
||||
return None
|
||||
|
||||
|
||||
def register(request, hook_recorder):
|
||||
request.addfinalizer(hook_recorder.finish_recording)
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const refsFrom = (file: string) => {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.qualifiedName);
|
||||
};
|
||||
|
||||
describe('member values through an untyped receiver', () => {
|
||||
it('are not a method whose owner the receiver is not named after', () => {
|
||||
expect(refsFrom('app/components.py')).not.toContain('FHRPGroupAssignmentFilter::device');
|
||||
});
|
||||
|
||||
it('are a method whose owner the receiver is named after', () => {
|
||||
expect(refsFrom('app/recorder.py')).toContain('HookRecorder::finish_recording');
|
||||
});
|
||||
});
|
||||
@@ -325,8 +325,14 @@ function matchMemberFunctionRef(ref: UnresolvedRef, context: ResolutionContext):
|
||||
}
|
||||
}
|
||||
// Unknown receivers retain the old unique-or-drop discipline, across ALL
|
||||
// files. Tests and abstract-looking bodies are candidates too.
|
||||
return result(context.getNodesByName(member), 0.8);
|
||||
// files. Tests and abstract-looking bodies are candidates too. A lone method
|
||||
// stands only when the receiver is named after its owner: netbox's
|
||||
// `device=self.parent.device` is a model field, not the project's one
|
||||
// `device` method (a GraphQL filter's). A veto, never a filter — filtering
|
||||
// first would promote some other lone match into a new guess.
|
||||
const unique = result(context.getNodesByName(member), 0.8);
|
||||
const target = unique ? context.getNodeById?.(unique.targetNodeId) : null;
|
||||
return target && target.kind === 'method' && !sharesReceiverWord(receiverLink(receiver), target) ? null : unique;
|
||||
}
|
||||
|
||||
function pythonRefClass(name: string, ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
||||
|
||||
Reference in New Issue
Block a user