fix(js,ts): static and instance methods override only their own kind (#2813)

The override-dispatch pass (interface-impl) linked a base class's
method to every subclass method of the same name, so a static method
counted as overriding an instance one, or the reverse, though a call to
one never runs the other (issue #2761). TypeScript nodes recorded
static already, so there the pass alone was wrong. JavaScript nodes
never did: the extractor had no isStatic hook and the kernel answered
nothing for the JS variants, so a JS class's static s() and s() were
both stored as instance methods.

JavaScript members now record their own static keyword with the hook
TypeScript uses (hasStaticModifier, which also keeps a method named
static() an instance one), and the kernel gives the JS variants the
same answer, so both engines agree. The pass skips a JS/TS base and
override whose static flags differ; a static method still overrides a
static one, and an instance method an instance one.

Validation: the new test fails 5 of 10 on main 08478eea (JS and JSX
static flags in both engines, and the override pairs) and passes 10 of
10 on the fix. A/B against main on 20 repos: webpack's lib loses 15
edges, every one a cross-kind pair, such as Module's instance
deserialize(context) to the static deserialize(context) factories of
NormalModule and six other modules, or Module's static
getSourceBasicTypes(module) to RuntimeModule's instance
getSourceBasicTypes(); the same-kind pairs stay and no edge is added.
No other repo's edges change. Nodes change only in is_static, 0 to 1,
on 315 JavaScript static members, none in TypeScript or another
language; the flask and gin control dumps are byte-identical. Kernel
parity over the 20 TS/JS repos: 16,612 of 16,873 files byte-parity,
none with diffs, the rest deferred to wasm as before. Index time,
median of 3 interleaved runs on vite, nuxt, webdriverio, insomnia and
webpack: -1.1% total, within noise.

Fixes #2761

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 20:08:41 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent efb091ec2f
commit 68fcf2f1c7
6 changed files with 213 additions and 15 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';
import { classifyTsClassMember } from './typescript';
import { classifyTsClassMember, hasStaticModifier } from './typescript';
export const javascriptExtractor: LanguageExtractor = {
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
@@ -77,6 +77,9 @@ export const javascriptExtractor: LanguageExtractor = {
}
return false;
},
// A `static` method or field, as in TypeScript (#2761): without it every JS
// class member read as an instance one.
isStatic: hasStaticModifier,
isConst: (node) => {
if (node.type === 'lexical_declaration') {
for (let i = 0; i < node.childCount; i++) {
+13 -7
View File
@@ -3,6 +3,18 @@ import type { LanguageExtractor } from '../tree-sitter-types';
import { hasFlowPragma } from '../grammars';
import type { Node as SyntaxNode } from 'web-tree-sitter';
/**
* Whether a class member is written `static`: its own `static` keyword, never
* a member named `static()`. Shared by the TypeScript and JavaScript
* extractors, which parse class bodies alike.
*/
export function hasStaticModifier(node: SyntaxNode): boolean {
for (let i = 0; i < node.childCount; i++) {
if (node.child(i)?.type === 'static') return true;
}
return false;
}
/**
* A TS/JS class field (`public_field_definition` / `field_definition`) is a
* METHOD only when its value is callable — an arrow function, a function
@@ -230,13 +242,7 @@ export const typescriptExtractor: LanguageExtractor = {
}
return false;
},
isStatic: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'static') return true;
}
return false;
},
isStatic: hasStaticModifier,
isConst: (node) => {
// For lexical_declaration, check if it's 'const' or 'let'
// For variable_declaration, it's always 'var'
+12
View File
@@ -822,6 +822,13 @@ const IFACE_OVERRIDE_LANGS = new Set([
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
'arkts',
]);
/**
* Languages whose static and instance methods override only their own kind:
* a JS/TS class's statics are inherited and overridden by the subclass's
* statics, its instance methods by instance methods (#2761). tsx and jsx
* follow the same rule, though this pass doesn't walk their classes today.
*/
const STATIC_SPLIT_LANGS: ReadonlySet<string> = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
/**
* Whether a supertype edge out of a Go type is one of its embeddings (#2397):
* declared, never synthesized (go-implements writes those), into a struct, an
@@ -1671,6 +1678,11 @@ 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 JS/TS static method overrides only a static one, and an
// instance method only an instance one (#2761): B's `s()` and the
// `static s()` of `class A extends B` are unrelated, and a call to
// one never runs the other.
if (STATIC_SPLIT_LANGS.has(m.language) && (bm.isStatic === true) !== (m.isStatic === true)) continue;
const key = `${bm.id}>${m.id}`;
if (seen.has(key)) continue;
seen.add(key);