fix(ts): a class extends the class after its type parameters (#2648)

fix(ts): a class extends the class after its type parameters

A TS/JS class head was read up to its first `extends`, which for
`class EditorPane<MementoType extends object = object> extends
Composite<MementoType>` is a type parameter's constraint: EditorPane read
as extending `object` (so nothing), and `MultiWindowParts<T extends
IMultiWindowPart, …>` as extending `IMultiWindowPart`. Fields, wrapper
methods and static methods inherited through such a head were missed or
found on the wrong class. The head is now read from its `class` keyword to
its body, over several lines, with type parameter lists and type arguments
left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 08:11:37 +00:00
committed by GitHub
parent 6071c9e487
commit d3a778d0b9
3 changed files with 145 additions and 2 deletions
+27 -2
View File
@@ -13467,12 +13467,37 @@ function readDeclaredSupertypes(cls: Node, context: ResolutionContext): string[]
case 'pascal': return names(/=\s*class\s*\(([^)]*)\)/i.exec(head)?.[1]);
case 'python': return names(/\bclass\s+\w+\s*\(([^)]*)\)/.exec(head)?.[1]?.replace(/\w+\s*=\s*[\w.]+/g, ''));
case 'ruby': return names(/\bclass\s+[\w:]+\s*<\s*([\w:]+)/.exec(head)?.[1]);
case 'php': case 'typescript': case 'tsx': case 'javascript': case 'jsx':
return names(/\bextends\s+([\w.\\]+)/.exec(head)?.[1]);
case 'php': return names(/\bextends\s+([\w.\\]+)/.exec(head)?.[1]);
case 'typescript': case 'tsx': case 'javascript': case 'jsx':
return names(/\bextends\s+([\w.\\]+)/.exec(tsClassHead(lines, cls))?.[1]);
default: return [];
}
}
/**
* A TS/JS class's head, from its `class` keyword to its body, with type
* parameter lists and type arguments left out: the first `extends` in
* `class EditorPane<M extends object = object> extends Composite<M>` is a
* constraint, not the class it extends. Decorators above the keyword are
* skipped, and the head may run over several lines.
*/
function tsClassHead(lines: readonly string[], cls: Node): string {
const text = lines.slice(cls.startLine - 1, cls.startLine + 7).join(' ');
const named = new RegExp(`\\bclass\\s+${cls.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).exec(text);
let head = '';
let angles = 0;
for (let i = named?.index ?? /\bclass\b/.exec(text)?.index ?? 0; i < text.length; i++) {
const c = text[i]!;
if (c === '<') angles++;
else if (c === '>' && angles > 0 && text[i - 1] !== '=') angles--;
else if (angles === 0) {
if (c === '{') break;
head += c;
}
}
return head;
}
/** A method named `name` on a supertype of the given classes, nearest first. */
function inheritedClassMethod(classes: Node[], name: string, context: ResolutionContext): Node | null {
if (classes.length === 0) return null;