Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- In JavaScript, a `static` class method is now recorded as static, as it already was in TypeScript; and in both languages a static method and an instance method of the same name are no longer linked as an override pair, so a call to one no longer reaches the other. (#2761)
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
Expand Down
92 changes: 92 additions & 0 deletions __tests__/static-instance-override.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* A static method and an instance method of the same name are not an override
* pair: `A.s()` never runs `B.prototype.s`, and `new A().s()` never runs
* `A.s`. The interface-impl synthesizer matched by name alone, so `B::s ->
* A::s` was drawn for `class B { s() {} }` / `class A extends B { static s()
* {} }`. In JavaScript a second cause: methods never recorded `isStatic` at
* all (TypeScript's extractor had the hook, JavaScript's did not), so even a
* static/static pair looked like instance/instance (#2761).
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-static-override-'));
const files: Record<string, string> = {
'ts/a.ts': `export class B {
s(): void {}
static t(): void {}
u(): void {}
}
export class A extends B {
static s(): void {}
t(): void {}
u(): void {}
}
export class S extends B {
static t(): void {}
}
`,
'js/a.js': `export class B {
static s() {}
s() {}
}
export class A extends B {
static s() {}
}
export class C extends B {
s() {}
}
`,
};
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 = CodeGraph.initSync(root);
await cg.indexAll();
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `Base::m -> Sub::m` of every interface-impl edge drawn inside `file`, with each end's static flag. */
function overridePairs(file: string): string[] {
const nodes = cg.getNodesInFile(file);
const byId = new Map(nodes.map((n) => [n.id, n]));
return cg.getOutgoingEdgesFrom(nodes.map((n) => n.id))
.filter((e) => e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl')
.map((e) => {
const s = byId.get(e.source)!;
const t = cg.getNode(e.target)!;
const tag = (n: typeof s) => `${n.qualifiedName}${n.isStatic ? '[static]' : ''}`;
return `${tag(s)} -> ${tag(t)}`;
})
.sort();
}

describe('a static and an instance method of one name are not an override pair (#2761)', () => {
it('TypeScript: only same-kind pairs are linked', () => {
expect(overridePairs('ts/a.ts')).toEqual([
'B::t[static] -> S::t[static]',
'B::u -> A::u',
]);
});

it('JavaScript: methods record `static`, and only same-kind pairs are linked', () => {
const statics = cg.getNodesInFile('js/a.js').filter((n) => n.kind === 'method').map((n) => `${n.qualifiedName}${n.isStatic ? '[static]' : ''}`).sort();
expect(statics).toEqual(['A::s[static]', 'B::s', 'B::s[static]', 'C::s']);
expect(overridePairs('js/a.js')).toEqual([
'B::s -> C::s',
'B::s[static] -> A::s[static]',
]);
});
});
9 changes: 3 additions & 6 deletions codegraph-kernel/src/tsjs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,13 +1072,10 @@ impl<'t> Walker<'t> {
self.has_keyword_child(node, "async")
}

/// TS has an isStatic hook; JS does not (None = field absent).
/// Both extractors read the `static` keyword child — JS's hook is TS's
/// since #2761.
fn is_static(&self, node: Node) -> Option<bool> {
if self.variant.is_ts() {
Some(self.has_keyword_child(node, "static"))
} else {
None
}
Some(self.has_keyword_child(node, "static"))
}

fn is_const_decl(&self, node: Node) -> bool {
Expand Down
6 changes: 5 additions & 1 deletion src/extraction/languages/javascript.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';
import { classifyTsClassMember } from './typescript';
import { classifyTsClassMember, typescriptExtractor } from './typescript';

export const javascriptExtractor: LanguageExtractor = {
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
Expand All @@ -9,6 +9,10 @@ export const javascriptExtractor: LanguageExtractor = {
// JS `field_definition` ≙ TS `public_field_definition`: plain fields are
// properties, function-valued fields are methods (#808).
classifyMethodNode: classifyTsClassMember,
// `static` is the same keyword child in both grammars. Without it every JS
// method was stored as an instance method, and a static `s()` paired with
// the instance `s()` of a subclass as an override (#2761).
isStatic: typescriptExtractor.isStatic,
interfaceTypes: [],
structTypes: [],
enumTypes: [],
Expand Down
3 changes: 3 additions & 0 deletions src/resolution/callback-synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,9 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield
for (const m of impls) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
if (bm.id === m.id) continue;
// A static method and an instance method of one name are not an
// override pair: a call to one never runs the other (#2761).
if ((bm.isStatic ?? false) !== (m.isStatic ?? false)) continue;
const key = `${bm.id}>${m.id}`;
if (seen.has(key)) continue;
seen.add(key);
Expand Down