Skip to content

Commit

Permalink
fix: Detect recursive types early (#2634)
Browse files Browse the repository at this point in the history
  • Loading branch information
dcodeIO committed Jan 24, 2023
1 parent e06c7bc commit bcfb5e8
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 59 deletions.
3 changes: 3 additions & 0 deletions src/ast.ts
Expand Up @@ -834,6 +834,9 @@ export abstract class TypeNode extends Node {
super(kind, range);
}

/** Whether this type node is currently in the process of being resolved. */
currentlyResolving: bool = false;

/** Tests if this type has a generic component matching one of the given type parameters. */
hasGenericComponent(typeParameterNodes: TypeParameterNode[]): bool {
if (this.kind == NodeKind.NamedType) {
Expand Down
81 changes: 34 additions & 47 deletions src/parser.ts
Expand Up @@ -3519,40 +3519,6 @@ export class Parser extends DiagnosticEmitter {
return null;
}

private getRecursiveDepthForTypeDeclaration(
identifierName: string,
type: TypeNode,
depth: i32 = 0
): i32 {
switch (type.kind) {
case NodeKind.NamedType: {
let typeArguments = (<NamedTypeNode>type).typeArguments;
if (typeArguments) {
for (let i = 0, k = typeArguments.length; i < k; i++) {
let res = this.getRecursiveDepthForTypeDeclaration(identifierName, typeArguments[i], depth + 1);
if (res != -1) return res;
}
}
if ((<NamedTypeNode>type).name.identifier.text == identifierName) {
return depth;
}
break;
}
case NodeKind.FunctionType: {
let fnType = <FunctionTypeNode>type;
let res = this.getRecursiveDepthForTypeDeclaration(identifierName, fnType.returnType, depth + 1);
if (res != -1) return res;
let params = fnType.parameters;
for (let i = 0, k = params.length; i < k; i++) {
res = this.getRecursiveDepthForTypeDeclaration(identifierName, params[i].type, depth + 1);
if (res != -1) return res;
}
break;
}
}
return -1;
}

parseTypeDeclaration(
tn: Tokenizer,
flags: CommonFlags,
Expand All @@ -3574,19 +3540,11 @@ export class Parser extends DiagnosticEmitter {
tn.skip(Token.Bar);
let type = this.parseType(tn);
if (!type) return null;
let depth = this.getRecursiveDepthForTypeDeclaration(name.text, type);
if (depth >= 0) {
if (depth == 0) {
this.error(
DiagnosticCode.Type_alias_0_circularly_references_itself,
tn.range(), name.text
);
} else {
this.error(
DiagnosticCode.Not_implemented_0,
tn.range(), "Recursion in type aliases"
);
}
if (isCircularTypeAlias(name.text, type)) {
this.error(
DiagnosticCode.Type_alias_0_circularly_references_itself,
name.range, name.text
);
return null;
}
let ret = Node.createTypeDeclaration(
Expand Down Expand Up @@ -4593,3 +4551,32 @@ function determinePrecedence(kind: Token): Precedence {
}
return Precedence.None;
}

/** Checks if the type alias of the given name and type is circular. */
function isCircularTypeAlias(name: string, type: TypeNode): bool {
switch (type.kind) {
case NodeKind.NamedType: {
if ((<NamedTypeNode>type).name.identifier.text == name) {
return true;
}
let typeArguments = (<NamedTypeNode>type).typeArguments;
if (typeArguments) {
for (let i = 0, k = typeArguments.length; i < k; i++) {
if (isCircularTypeAlias(name, typeArguments[i])) return true;
}
}
break;
}
case NodeKind.FunctionType: {
let functionType = <FunctionTypeNode>type;
if (isCircularTypeAlias(name, functionType.returnType)) return true;
let parameters = functionType.parameters;
for (let i = 0, k = parameters.length; i < k; i++) {
if (isCircularTypeAlias(name, parameters[i].type)) return true;
}
break;
}
default: assert(false);
}
return false;
}
20 changes: 16 additions & 4 deletions src/resolver.ts
Expand Up @@ -149,26 +149,38 @@ export class Resolver extends DiagnosticEmitter {
/** How to proceed with eventual diagnostics. */
reportMode: ReportMode = ReportMode.Report
): Type | null {
if (node.currentlyResolving) {
this.error(
DiagnosticCode.Not_implemented_0,
node.range, "Recursive types"
);
return null;
}
node.currentlyResolving = true;
let resolved: Type | null = null;
switch (node.kind) {
case NodeKind.NamedType: {
return this.resolveNamedType(
resolved = this.resolveNamedType(
<NamedTypeNode>node,
ctxElement,
ctxTypes,
reportMode
);
break;
}
case NodeKind.FunctionType: {
return this.resolveFunctionType(
resolved = this.resolveFunctionType(
<FunctionTypeNode>node,
ctxElement,
ctxTypes,
reportMode
);
break;
}
default: assert(false);
}
return null;
node.currentlyResolving = false;
return resolved;
}

/** Resolves a {@link NamedTypeNode} to a concrete {@link Type}. */
Expand Down Expand Up @@ -2721,7 +2733,7 @@ export class Resolver extends DiagnosticEmitter {
const declaration = node.declaration;
const signature = declaration.signature;
const body = declaration.body;
let functionType = this.resolveFunctionType(signature, ctxFlow.sourceFunction, ctxFlow.contextualTypeArguments, reportMode);
let functionType = this.resolveType(signature, ctxFlow.sourceFunction, ctxFlow.contextualTypeArguments, reportMode);
if (
functionType &&
declaration.arrowKind != ArrowKind.None &&
Expand Down
6 changes: 6 additions & 0 deletions tests/compiler/typerecursion.json
@@ -0,0 +1,6 @@
{
"stderr": [
"AS100: Not implemented: Recursive types",
"EOF"
]
}
6 changes: 6 additions & 0 deletions tests/compiler/typerecursion.ts
@@ -0,0 +1,6 @@
type RecMethod = () => RecReturn;
type RecReturn = RecMethod | null;

const test: RecMethod = () => null;

ERROR("EOF");
16 changes: 8 additions & 8 deletions tests/parser/type.ts.fixture.ts
Expand Up @@ -5,11 +5,11 @@ export type T1 = int32_t;
export type T2 = int32_t;
export type T11 = T1 | null;
export type T12 = T1 | null;
// ERROR 2456: "Type alias 'T3' circularly references itself." in type.ts(11,23+4)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(12,29+3)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(13,24+2)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(14,31+1)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(15,26+1)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(16,39+1)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(17,32+1)
// ERROR 100: "Not implemented: Recursion in type aliases" in type.ts(18,25+1)
// ERROR 2456: "Type alias 'T3' circularly references itself." in type.ts(11,13+2)
// ERROR 2456: "Type alias 'T4' circularly references itself." in type.ts(12,13+2)
// ERROR 2456: "Type alias 'T5' circularly references itself." in type.ts(13,13+2)
// ERROR 2456: "Type alias 'T6' circularly references itself." in type.ts(14,13+2)
// ERROR 2456: "Type alias 'T7' circularly references itself." in type.ts(15,13+2)
// ERROR 2456: "Type alias 'T8' circularly references itself." in type.ts(16,13+2)
// ERROR 2456: "Type alias 'T9' circularly references itself." in type.ts(17,13+2)
// ERROR 2456: "Type alias 'T10' circularly references itself." in type.ts(18,13+3)

0 comments on commit bcfb5e8

Please sign in to comment.