mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 20:12:11 +08:00
fix(js,ts): a named class expression is a class with its own members (#2825)
A named class expression, like register(class Widget extends Base {})
or module.exports = class Router {}, was not indexed as a class, as
issue #2499 found. Its methods became free functions, its fields and
supertypes were dropped, and calls in it had no class to start from,
so its this-calls, super calls and own static calls went unresolved or
to a by-name guess, and calls elsewhere guessed into its methods.
Both engines now index a named class expression as a class, with its
methods, properties, extends and implements. An anonymous one is still
passed over. Its head (class Widget extends Base) is its signature,
which marks it as an expression, and it is never exported by itself.
JavaScript binds the name only inside the expression, and the resolver
now scopes it the same way. A reference written outside every class
expression resolves in a view of the graph that hides class
expressions and their members, so new Widget() elsewhere links
nothing, and a same-named class, function or binding keeps its links.
A name only class expressions carry is unknown there. A reference
inside one sees the whole graph, unless it names another class
expression. A class a CommonJS module exports, module.exports = class
Router {} or exports.Store = class Store {}, is the module's value,
which require hands to every importer, so it is not scoped: it reads
as a declaration does, and a require of it reaches its static methods
and its instances' methods. The reader of CommonJS named exports takes
exports.X = class Y {} too. Super calls in a class expression now
resolve through the super-call matcher.
Validation: the new test fails 11 of 13 on main d5fc1fbb and passes 13
of 13 on the fix, and a mutation of each of 7 parts (the view, the
inside check, the name rule, the expression marker, the class node,
the CommonJS export rule, the named-export reader) fails a test.
A/B against main d5fc1fbb on 22 repos (20 TS/JS, flask and gin): 193
classes are added in 10 repos with their members, and 801 edge sites
change in 7 repos, each bucketed and sampled by hand. 563 are jest's
calls to its global test(), 0.4 guesses that move from one wrong
namesake to another: the old one, a class expression's test() in an
e2e fixture, is now a method. Of the rest, 85 are new extends,
implements and type references of class expressions; 74 calls, news
and references move to the member they are written in; 65 calls and
news gain or change a target, inside class expressions (this-calls,
own static calls) and through require (static calls, typed instance
calls, new); and 14 guesses are removed, among them 3 super calls
whose parent the index can't name. 12 repos' dumps, the controls among
them, are byte-identical. Kernel parity holds on 22 repos. Three sync
scenarios match a fresh index. Index time, median of 5 interleaved
runs: lit +0.8%, webpack +2.7%, jest +0.6%; insomnia +12.8%, though it
indexes byte-identically, holds no class expression, and its resolver
time is the same on both arms, so that gap is other jobs' load.
Fixes #2499
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23a80a847a
commit
013b91c1be
@@ -4,7 +4,8 @@ import { classifyTsClassMember, hasStaticModifier } from './typescript';
|
||||
|
||||
export const javascriptExtractor: LanguageExtractor = {
|
||||
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
|
||||
classTypes: ['class_declaration'],
|
||||
// A named class expression too, as in TypeScript (#2499).
|
||||
classTypes: ['class_declaration', 'class'],
|
||||
methodTypes: ['method_definition', 'field_definition'],
|
||||
// JS `field_definition` ≙ TS `public_field_definition`: plain fields are
|
||||
// properties, function-valued fields are methods (#808).
|
||||
|
||||
@@ -150,7 +150,10 @@ export const typescriptExtractor: LanguageExtractor = {
|
||||
preParse: blankFlowSyntax,
|
||||
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
|
||||
isAmbientFunction: isAmbientFunctionSignature,
|
||||
classTypes: ['class_declaration', 'abstract_class_declaration'],
|
||||
// `class` is a class expression, `register(class Widget extends Base {…})`:
|
||||
// a named one is a class with its members and supertypes (#2499), and an
|
||||
// anonymous one is passed over (TreeSitterExtractor.isClassTypeNode).
|
||||
classTypes: ['class_declaration', 'abstract_class_declaration', 'class'],
|
||||
// `method_signature` is the interface/type-literal form of a method; without it
|
||||
// an interface's members never enter the graph, so a `.d.ts` platform API has
|
||||
// no declaration node for call sites to attach to (#1638). Java/C# don't need
|
||||
|
||||
@@ -430,6 +430,14 @@ const LITERAL_RECEIVER_TYPES = new Set([
|
||||
*/
|
||||
const TS_JS_CHAIN_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
|
||||
|
||||
/** Languages whose `class` node is a class expression (#2499). */
|
||||
const CLASS_EXPRESSION_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
|
||||
|
||||
/** A class expression's head, `class Widget extends Base`: its text up to the body. Mirrored in the kernel (tsjs extract_class). */
|
||||
function classExpressionHead(source: string, node: SyntaxNode, body: SyntaxNode): string {
|
||||
return source.substring(node.startIndex, body.startIndex).replace(/[ \t\r\n]+$/, '');
|
||||
}
|
||||
|
||||
/** Receiver node types (TS/JS grammars) that continue a member chain downward. */
|
||||
const TS_JS_CHAIN_RECEIVER_TYPES = new Set(['member_expression', 'subscript_expression']);
|
||||
|
||||
@@ -1543,7 +1551,7 @@ export class TreeSitterExtractor {
|
||||
skipChildren = true;
|
||||
}
|
||||
// Check for class declarations
|
||||
else if (this.extractor.classTypes.includes(nodeType)) {
|
||||
else if (this.isClassTypeNode(node, nodeType)) {
|
||||
// Some languages reuse class_declaration for structs/enums (e.g. Swift)
|
||||
const classification = this.extractor.classifyClassNode?.(node) ?? 'class';
|
||||
if (classification === 'struct') {
|
||||
@@ -2540,6 +2548,16 @@ export class TreeSitterExtractor {
|
||||
/**
|
||||
* Extract a class
|
||||
*/
|
||||
/**
|
||||
* Whether `node` is a class to extract: one of the language's class types,
|
||||
* except a TS/JS class expression without a name (`register(class extends
|
||||
* Base {…})`), which is passed over as before (#2499).
|
||||
*/
|
||||
private isClassTypeNode(node: SyntaxNode, nodeType: string): boolean {
|
||||
if (!this.extractor!.classTypes.includes(nodeType)) return false;
|
||||
return nodeType !== 'class' || !CLASS_EXPRESSION_LANGUAGES.has(this.language) || !!getChildByField(node, 'name');
|
||||
}
|
||||
|
||||
private extractClass(node: SyntaxNode, kind: NodeKind = 'class'): void {
|
||||
if (!this.extractor) return;
|
||||
|
||||
@@ -2555,13 +2573,19 @@ export class TreeSitterExtractor {
|
||||
const name = extractName(node, this.source, this.extractor);
|
||||
const docstring = this.docstringFor(node);
|
||||
const visibility = this.extractor.getVisibility?.(node);
|
||||
const isExported = this.extractor.isExported?.(node, this.source);
|
||||
// A class expression's name is bound inside the expression alone, so the
|
||||
// class is never what a module exports by it, and it carries its head
|
||||
// (`class Widget extends Base`) as its signature: a class declaration
|
||||
// has none, which is how resolution tells the two apart (#2499).
|
||||
const expression = node.type === 'class' && CLASS_EXPRESSION_LANGUAGES.has(this.language);
|
||||
const isExported = expression ? false : this.extractor.isExported?.(node, this.source);
|
||||
const scoped = this.writtenScopeQualifiedName(node, name);
|
||||
|
||||
const classNode = this.createNode(kind, name, node, {
|
||||
docstring,
|
||||
visibility,
|
||||
isExported,
|
||||
...(expression && resolvedBody ? { signature: classExpressionHead(this.source, node, resolvedBody) } : {}),
|
||||
...this.cppBodyEnd(resolvedBody),
|
||||
...(scoped !== undefined ? { qualifiedName: scoped } : {}),
|
||||
});
|
||||
@@ -7420,7 +7444,7 @@ export class TreeSitterExtractor {
|
||||
|
||||
// Extract structural nodes found inside function bodies.
|
||||
// Each extract method visits its own children, so we return after extracting.
|
||||
if (this.extractor!.classTypes.includes(nodeType)) {
|
||||
if (this.isClassTypeNode(node, nodeType)) {
|
||||
const classification = this.extractor!.classifyClassNode?.(node) ?? 'class';
|
||||
if (classification === 'struct') this.extractStruct(node);
|
||||
else if (classification === 'enum') this.extractEnum(node);
|
||||
|
||||
Reference in New Issue
Block a user