Skip to content

Commit bcfb5e8

Browse files
authored
fix: Detect recursive types early (#2634)
1 parent e06c7bc commit bcfb5e8

File tree

6 files changed

+73
-59
lines changed

6 files changed

+73
-59
lines changed

Diff for: src/ast.ts

+3
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,9 @@ export abstract class TypeNode extends Node {
834834
super(kind, range);
835835
}
836836

837+
/** Whether this type node is currently in the process of being resolved. */
838+
currentlyResolving: bool = false;
839+
837840
/** Tests if this type has a generic component matching one of the given type parameters. */
838841
hasGenericComponent(typeParameterNodes: TypeParameterNode[]): bool {
839842
if (this.kind == NodeKind.NamedType) {

Diff for: src/parser.ts

+34-47
Original file line numberDiff line numberDiff line change
@@ -3519,40 +3519,6 @@ export class Parser extends DiagnosticEmitter {
35193519
return null;
35203520
}
35213521

3522-
private getRecursiveDepthForTypeDeclaration(
3523-
identifierName: string,
3524-
type: TypeNode,
3525-
depth: i32 = 0
3526-
): i32 {
3527-
switch (type.kind) {
3528-
case NodeKind.NamedType: {
3529-
let typeArguments = (<NamedTypeNode>type).typeArguments;
3530-
if (typeArguments) {
3531-
for (let i = 0, k = typeArguments.length; i < k; i++) {
3532-
let res = this.getRecursiveDepthForTypeDeclaration(identifierName, typeArguments[i], depth + 1);
3533-
if (res != -1) return res;
3534-
}
3535-
}
3536-
if ((<NamedTypeNode>type).name.identifier.text == identifierName) {
3537-
return depth;
3538-
}
3539-
break;
3540-
}
3541-
case NodeKind.FunctionType: {
3542-
let fnType = <FunctionTypeNode>type;
3543-
let res = this.getRecursiveDepthForTypeDeclaration(identifierName, fnType.returnType, depth + 1);
3544-
if (res != -1) return res;
3545-
let params = fnType.parameters;
3546-
for (let i = 0, k = params.length; i < k; i++) {
3547-
res = this.getRecursiveDepthForTypeDeclaration(identifierName, params[i].type, depth + 1);
3548-
if (res != -1) return res;
3549-
}
3550-
break;
3551-
}
3552-
}
3553-
return -1;
3554-
}
3555-
35563522
parseTypeDeclaration(
35573523
tn: Tokenizer,
35583524
flags: CommonFlags,
@@ -3574,19 +3540,11 @@ export class Parser extends DiagnosticEmitter {
35743540
tn.skip(Token.Bar);
35753541
let type = this.parseType(tn);
35763542
if (!type) return null;
3577-
let depth = this.getRecursiveDepthForTypeDeclaration(name.text, type);
3578-
if (depth >= 0) {
3579-
if (depth == 0) {
3580-
this.error(
3581-
DiagnosticCode.Type_alias_0_circularly_references_itself,
3582-
tn.range(), name.text
3583-
);
3584-
} else {
3585-
this.error(
3586-
DiagnosticCode.Not_implemented_0,
3587-
tn.range(), "Recursion in type aliases"
3588-
);
3589-
}
3543+
if (isCircularTypeAlias(name.text, type)) {
3544+
this.error(
3545+
DiagnosticCode.Type_alias_0_circularly_references_itself,
3546+
name.range, name.text
3547+
);
35903548
return null;
35913549
}
35923550
let ret = Node.createTypeDeclaration(
@@ -4593,3 +4551,32 @@ function determinePrecedence(kind: Token): Precedence {
45934551
}
45944552
return Precedence.None;
45954553
}
4554+
4555+
/** Checks if the type alias of the given name and type is circular. */
4556+
function isCircularTypeAlias(name: string, type: TypeNode): bool {
4557+
switch (type.kind) {
4558+
case NodeKind.NamedType: {
4559+
if ((<NamedTypeNode>type).name.identifier.text == name) {
4560+
return true;
4561+
}
4562+
let typeArguments = (<NamedTypeNode>type).typeArguments;
4563+
if (typeArguments) {
4564+
for (let i = 0, k = typeArguments.length; i < k; i++) {
4565+
if (isCircularTypeAlias(name, typeArguments[i])) return true;
4566+
}
4567+
}
4568+
break;
4569+
}
4570+
case NodeKind.FunctionType: {
4571+
let functionType = <FunctionTypeNode>type;
4572+
if (isCircularTypeAlias(name, functionType.returnType)) return true;
4573+
let parameters = functionType.parameters;
4574+
for (let i = 0, k = parameters.length; i < k; i++) {
4575+
if (isCircularTypeAlias(name, parameters[i].type)) return true;
4576+
}
4577+
break;
4578+
}
4579+
default: assert(false);
4580+
}
4581+
return false;
4582+
}

Diff for: src/resolver.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,38 @@ export class Resolver extends DiagnosticEmitter {
149149
/** How to proceed with eventual diagnostics. */
150150
reportMode: ReportMode = ReportMode.Report
151151
): Type | null {
152+
if (node.currentlyResolving) {
153+
this.error(
154+
DiagnosticCode.Not_implemented_0,
155+
node.range, "Recursive types"
156+
);
157+
return null;
158+
}
159+
node.currentlyResolving = true;
160+
let resolved: Type | null = null;
152161
switch (node.kind) {
153162
case NodeKind.NamedType: {
154-
return this.resolveNamedType(
163+
resolved = this.resolveNamedType(
155164
<NamedTypeNode>node,
156165
ctxElement,
157166
ctxTypes,
158167
reportMode
159168
);
169+
break;
160170
}
161171
case NodeKind.FunctionType: {
162-
return this.resolveFunctionType(
172+
resolved = this.resolveFunctionType(
163173
<FunctionTypeNode>node,
164174
ctxElement,
165175
ctxTypes,
166176
reportMode
167177
);
178+
break;
168179
}
169180
default: assert(false);
170181
}
171-
return null;
182+
node.currentlyResolving = false;
183+
return resolved;
172184
}
173185

174186
/** Resolves a {@link NamedTypeNode} to a concrete {@link Type}. */
@@ -2721,7 +2733,7 @@ export class Resolver extends DiagnosticEmitter {
27212733
const declaration = node.declaration;
27222734
const signature = declaration.signature;
27232735
const body = declaration.body;
2724-
let functionType = this.resolveFunctionType(signature, ctxFlow.sourceFunction, ctxFlow.contextualTypeArguments, reportMode);
2736+
let functionType = this.resolveType(signature, ctxFlow.sourceFunction, ctxFlow.contextualTypeArguments, reportMode);
27252737
if (
27262738
functionType &&
27272739
declaration.arrowKind != ArrowKind.None &&

Diff for: tests/compiler/typerecursion.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"stderr": [
3+
"AS100: Not implemented: Recursive types",
4+
"EOF"
5+
]
6+
}

Diff for: tests/compiler/typerecursion.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type RecMethod = () => RecReturn;
2+
type RecReturn = RecMethod | null;
3+
4+
const test: RecMethod = () => null;
5+
6+
ERROR("EOF");

Diff for: tests/parser/type.ts.fixture.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ export type T1 = int32_t;
55
export type T2 = int32_t;
66
export type T11 = T1 | null;
77
export type T12 = T1 | null;
8-
// ERROR 2456: "Type alias 'T3' circularly references itself." in type.ts(11,23+4)
9-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(12,29+3)
10-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(13,24+2)
11-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(14,31+1)
12-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(15,26+1)
13-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(16,39+1)
14-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(17,32+1)
15-
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(18,25+1)
8+
// ERROR 2456: "Type alias 'T3' circularly references itself." in type.ts(11,13+2)
9+
// ERROR 2456: "Type alias 'T4' circularly references itself." in type.ts(12,13+2)
10+
// ERROR 2456: "Type alias 'T5' circularly references itself." in type.ts(13,13+2)
11+
// ERROR 2456: "Type alias 'T6' circularly references itself." in type.ts(14,13+2)
12+
// ERROR 2456: "Type alias 'T7' circularly references itself." in type.ts(15,13+2)
13+
// ERROR 2456: "Type alias 'T8' circularly references itself." in type.ts(16,13+2)
14+
// ERROR 2456: "Type alias 'T9' circularly references itself." in type.ts(17,13+2)
15+
// ERROR 2456: "Type alias 'T10' circularly references itself." in type.ts(18,13+3)

0 commit comments

Comments
 (0)