Skip to content

Commit b696936

Browse files
authored
Merge pull request github#12921 from asgerf/js/typescript-compiler-crash
JS: Fix extractor crash related to recursive generic type alias
2 parents 8ca5484 + c9c281c commit b696936

File tree

6 files changed

+96
-4
lines changed

6 files changed

+96
-4
lines changed

javascript/extractor/lib/typescript/src/type_table.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ function isTypeAlwaysSafeToExpand(type: ts.Type): boolean {
4343
return false;
4444
}
4545
}
46+
if (type.aliasSymbol != null) {
47+
return false;
48+
}
4649
return true;
4750
}
4851

@@ -447,7 +450,7 @@ export class TypeTable {
447450
this.isInShallowTypeContext ? TypeExtractionState.PendingShallow : TypeExtractionState.PendingFull);
448451
// If the type is the self-type for a named type (not a generic instantiation of it),
449452
// emit the self-type binding for that type.
450-
if (content.startsWith("reference;") && !(isTypeReference(type) && type.target !== type)) {
453+
if (content.startsWith("reference;") && isTypeSelfReference(type)) {
451454
this.selfTypes.symbols.push(this.getSymbolId(type.aliasSymbol || type.symbol));
452455
this.selfTypes.selfTypes.push(id);
453456
}
@@ -533,7 +536,7 @@ export class TypeTable {
533536
let enclosingType = getEnclosingTypeOfThisType(type);
534537
if (enclosingType != null) {
535538
return "this;" + this.getId(enclosingType, false);
536-
} else if (symbol.parent == null) {
539+
} else if (symbol.parent == null || isFunctionTypeOrTypeAlias(symbol.declarations?.[0])) {
537540
// The type variable is bound on a call signature. Only extract it by name.
538541
return "lextypevar;" + symbol.name;
539542
} else {
@@ -614,14 +617,14 @@ export class TypeTable {
614617
// cannot be written using TypeScript syntax - so we ignore them entirely.
615618
return null;
616619
}
617-
return this.makeTypeStringVector("union", unionType.types);
620+
return this.makeDeduplicatedTypeStringVector("union", unionType.types);
618621
}
619622
if (flags & ts.TypeFlags.Intersection) {
620623
let intersectionType = type as ts.IntersectionType;
621624
if (intersectionType.types.length === 0) {
622625
return null; // Ignore malformed type.
623626
}
624-
return this.makeTypeStringVector("intersection", intersectionType.types);
627+
return this.makeDeduplicatedTypeStringVector("intersection", intersectionType.types);
625628
}
626629
if (isTypeReference(type) && (type.target.objectFlags & ts.ObjectFlags.Tuple)) {
627630
// Encode the minimum length and presence of rest element in the first two parts of the type string.
@@ -784,6 +787,27 @@ export class TypeTable {
784787
return hash;
785788
}
786789

790+
/**
791+
* Returns the given string with the IDs of the given types appended,
792+
* ignoring duplicates, and each separated by `;`.
793+
*/
794+
private makeDeduplicatedTypeStringVector(tag: string, types: ReadonlyArray<ts.Type>, length = types.length): string | null {
795+
let seenIds = new Set<number>();
796+
let numberOfSeenIds = 0;
797+
let hash = tag;
798+
for (let i = 0; i < length; ++i) {
799+
let id = this.getId(types[i], false);
800+
if (id == null) return null;
801+
seenIds.add(id);
802+
if (seenIds.size > numberOfSeenIds) {
803+
// This ID was not seen before
804+
++numberOfSeenIds;
805+
hash += ";" + id;
806+
}
807+
}
808+
return hash;
809+
}
810+
787811
/** Returns the type of `symbol` or `null` if it could not be computed. */
788812
private tryGetTypeOfSymbol(symbol: ts.Symbol) {
789813
try {
@@ -1328,3 +1352,35 @@ export class TypeTable {
13281352
}
13291353
}
13301354
}
1355+
1356+
function isFunctionTypeOrTypeAlias(declaration: ts.Declaration | undefined) {
1357+
if (declaration == null) return false;
1358+
return declaration.kind === ts.SyntaxKind.FunctionType || declaration.kind === ts.SyntaxKind.TypeAliasDeclaration;
1359+
}
1360+
1361+
/**
1362+
* Given a `type` whose type-string is known to be a `reference`, returns true if this is the self-type for the referenced type.
1363+
*
1364+
* For example, for `type Foo<R> = ...` this returns true if `type` is `Foo<R>`.
1365+
*/
1366+
function isTypeSelfReference(type: ts.Type) {
1367+
if (type.aliasSymbol != null) {
1368+
const { aliasTypeArguments } = type;
1369+
if (aliasTypeArguments == null) return true;
1370+
let declaration = type.aliasSymbol.declarations?.[0];
1371+
if (declaration == null || declaration.kind !== ts.SyntaxKind.TypeAliasDeclaration) return false;
1372+
let alias = declaration as ts.TypeAliasDeclaration;
1373+
for (let i = 0; i < aliasTypeArguments.length; ++i) {
1374+
if (aliasTypeArguments[i].symbol?.declarations?.[0] !== alias.typeParameters[i]) {
1375+
return false;
1376+
}
1377+
}
1378+
return true;
1379+
} else if (isTypeReference(type)) {
1380+
return type.target === type;
1381+
} else {
1382+
// Return true because we know we have mapped this type to kind `reference`, and in the cases
1383+
// not covered above (i.e. generic types) it is always a self-reference.
1384+
return true;
1385+
}
1386+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: fix
3+
---
4+
* Fixes an issue that would cause TypeScript extraction to hang in rare cases when extracting
5+
code containing recursive generic type aliases.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
typeAliases
2+
| tst.ts:1:8:1:41 | type Fo ... R \| A>; |
3+
| tst.ts:3:8:3:42 | type Ba ... R, A]>; |
4+
| tst.ts:5:8:5:47 | type Ba ... => A>; |
5+
typeAliasType
6+
| tst.ts:1:8:1:41 | type Fo ... R \| A>; | Foo<R> |
7+
| tst.ts:3:8:3:42 | type Ba ... R, A]>; | Bar<R> |
8+
| tst.ts:5:8:5:47 | type Ba ... => A>; | Baz<R> |
9+
getAliasedType
10+
| Bar<R> | <A>() => Bar<[R, A]> |
11+
| Bar<[R, A]> | <A>() => Bar<[[R, A], A]> |
12+
| Baz<(x: R) => A> | <A>() => Baz<(x: (x: R) => A) => A> |
13+
| Baz<R> | <A>() => Baz<(x: R) => A> |
14+
| Foo<R \| A> | <A>() => Foo<R \| A> |
15+
| Foo<R> | <A>() => Foo<R \| A> |
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import javascript
2+
3+
// The extractor would hang on this test case, it doesn't matter too much what the output of the test is.
4+
query TypeAliasDeclaration typeAliases() { any() }
5+
6+
query Type typeAliasType(TypeAliasDeclaration decl) { result = decl.getTypeName().getType() }
7+
8+
query Type getAliasedType(TypeAliasReference ref) { result = ref.getAliasedType() }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"include": ["."]
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type Foo<R> = <A>() => Foo<R | A>;
2+
3+
export type Bar<R> = <A>() => Bar<[R, A]>;
4+
5+
export type Baz<R> = <A>() => Baz<(x: R) => A>;

0 commit comments

Comments
 (0)