Skip to content

Commit

Permalink
feat: Constructor generic type inference (#2894)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Jan 21, 2025
1 parent 40850fe commit 9a7a6e0
Show file tree
Hide file tree
Showing 4 changed files with 4,569 additions and 112 deletions.
271 changes: 169 additions & 102 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,110 +731,158 @@ export class Resolver extends DiagnosticEmitter {

// infer generic call if type arguments have been omitted
if (prototype.is(CommonFlags.Generic)) {
let contextualTypeArguments = cloneMap(ctxFlow.contextualTypeArguments);
let resolvedTypeArguments = this.inferGenericTypeArguments(
node,
prototype,
prototype.typeParameterNodes,
ctxFlow,
reportMode,
);

return this.resolveFunction(
prototype,
resolvedTypeArguments,
cloneMap(ctxFlow.contextualTypeArguments),
reportMode
);
}

// fill up contextual types with auto for each generic component
let typeParameterNodes = assert(prototype.typeParameterNodes);
let numTypeParameters = typeParameterNodes.length;
let typeParameterNames = new Set<string>();
for (let i = 0; i < numTypeParameters; ++i) {
let name = typeParameterNodes[i].name.text;
contextualTypeArguments.set(name, Type.auto);
typeParameterNames.add(name);
}

let parameterNodes = prototype.functionTypeNode.parameters;
let numParameters = parameterNodes.length;
let argumentNodes = node.args;
let numArguments = argumentNodes.length;

// infer types with generic components while updating contextual types
for (let i = 0; i < numParameters; ++i) {
let argumentExpression = i < numArguments
? argumentNodes[i]
: parameterNodes[i].initializer;
if (!argumentExpression) {
// optional but not have initializer should be handled in the other place
if (parameterNodes[i].parameterKind == ParameterKind.Optional) {
continue;
}
// missing initializer -> too few arguments
if (reportMode == ReportMode.Report) {
this.error(
DiagnosticCode.Expected_0_arguments_but_got_1,
node.range, numParameters.toString(), numArguments.toString()
);
}
return null;
// otherwise resolve the non-generic call as usual
return this.resolveFunction(prototype, null, new Map(), reportMode);
}

private inferGenericTypeArguments(
node: Expression,
prototype: FunctionPrototype,
typeParameterNodes: TypeParameterNode[] | null,
ctxFlow: Flow,
reportMode: ReportMode = ReportMode.Report,
): Type[] | null {

if (!typeParameterNodes) {
return null;
}

let contextualTypeArguments = cloneMap(ctxFlow.contextualTypeArguments);

// fill up contextual types with auto for each generic component
let numTypeParameters = typeParameterNodes.length;
let typeParameterNames = new Set<string>();
for (let i = 0; i < numTypeParameters; ++i) {
let name = typeParameterNodes[i].name.text;
contextualTypeArguments.set(name, Type.auto);
typeParameterNames.add(name);
}

let parameterNodes = prototype.functionTypeNode.parameters;
let numParameters = parameterNodes.length;

let argumentNodes: Expression[];
switch (node.kind) {
case NodeKind.Call:
argumentNodes = (<CallExpression>node).args;
break;
case NodeKind.New:
argumentNodes = (<NewExpression>node).args;
break;
default:
assert(false);
return null;
}

let numArguments = argumentNodes.length;

// infer types with generic components while updating contextual types
for (let i = 0; i < numParameters; ++i) {
let argumentExpression = i < numArguments
? argumentNodes[i]
: parameterNodes[i].initializer;
if (!argumentExpression) {
// optional but not have initializer should be handled in the other place
if (parameterNodes[i].parameterKind == ParameterKind.Optional) {
continue;
}
// missing initializer -> too few arguments
if (reportMode == ReportMode.Report) {
this.error(
DiagnosticCode.Expected_0_arguments_but_got_1,
node.range, numParameters.toString(), numArguments.toString()
);
}
let typeNode = parameterNodes[i].type;
if (typeNode.hasGenericComponent(typeParameterNodes)) {
let type = this.resolveExpression(argumentExpression, ctxFlow, Type.auto, ReportMode.Swallow);
if (type) {
this.propagateInferredGenericTypes(
typeNode,
type,
prototype,
contextualTypeArguments,
typeParameterNames
);
}
return null;
}
let typeNode = parameterNodes[i].type;
if (typeNode.hasGenericComponent(typeParameterNodes)) {
let type = this.resolveExpression(argumentExpression, ctxFlow, Type.auto, ReportMode.Swallow);
if (type) {
this.propagateInferredGenericTypes(
typeNode,
type,
prototype,
contextualTypeArguments,
typeParameterNames
);
}
}
}

// apply concrete types to the generic function signature
let resolvedTypeArguments = new Array<Type>(numTypeParameters);
for (let i = 0; i < numTypeParameters; ++i) {
let typeParameterNode = typeParameterNodes[i];
let name = typeParameterNode.name.text;
if (contextualTypeArguments.has(name)) {
let inferredType = assert(contextualTypeArguments.get(name));
if (inferredType != Type.auto) {
resolvedTypeArguments[i] = inferredType;
continue;
}
let defaultType = typeParameterNode.defaultType;
if (defaultType) {
// Default parameters are resolved in context of the called function, not the calling function
let parent = prototype.parent;
let defaultTypeContextualTypeArguments: Map<string, Type> | null = null;
if (parent.kind == ElementKind.Class) {
defaultTypeContextualTypeArguments = (<Class>parent).contextualTypeArguments;
} else if (parent.kind == ElementKind.Function) {
defaultTypeContextualTypeArguments = (<Function>parent).contextualTypeArguments;
}
let resolvedDefaultType = this.resolveType(
defaultType,
null,
prototype,
defaultTypeContextualTypeArguments,
reportMode
);
if (!resolvedDefaultType) return null;
resolvedTypeArguments[i] = resolvedDefaultType;
continue;
// apply concrete types to the generic function signature
let resolvedTypeArguments = new Array<Type>(numTypeParameters);
for (let i = 0; i < numTypeParameters; ++i) {
let typeParameterNode = typeParameterNodes[i];
let name = typeParameterNode.name.text;
if (contextualTypeArguments.has(name)) {
let inferredType = assert(contextualTypeArguments.get(name));
if (inferredType != Type.auto) {
resolvedTypeArguments[i] = inferredType;
continue;
}
let defaultType = typeParameterNode.defaultType;
if (defaultType) {
// Default parameters are resolved in context of the called function, not the calling function
let parent = prototype.parent;
let defaultTypeContextualTypeArguments: Map<string, Type> | null = null;
if (parent.kind == ElementKind.Class) {
defaultTypeContextualTypeArguments = (<Class>parent).contextualTypeArguments;
} else if (parent.kind == ElementKind.Function) {
defaultTypeContextualTypeArguments = (<Function>parent).contextualTypeArguments;
}
}
// unused template, e.g. `function test<T>(): void {...}` called as `test()`
// invalid because the type is effectively unknown inside the function body
if (reportMode == ReportMode.Report) {
this.error(
DiagnosticCode.Type_argument_expected,
node.expression.range.atEnd
let resolvedDefaultType = this.resolveType(
defaultType,
null,
prototype,
defaultTypeContextualTypeArguments,
reportMode
);
if (!resolvedDefaultType) return null;
resolvedTypeArguments[i] = resolvedDefaultType;
continue;
}
return null;
}
return this.resolveFunction(
prototype,
resolvedTypeArguments,
cloneMap(ctxFlow.contextualTypeArguments),
reportMode
);
// unused template, e.g. `function test<T>(): void {...}` called as `test()`
// invalid because the type is effectively unknown inside the function body
if (reportMode == ReportMode.Report) {
let range: Range;
switch (node.kind) {
case NodeKind.Call:
range = (<CallExpression>node).expression.range;
break;
case NodeKind.New:
range = (<NewExpression>node).typeName.range;
break;
default:
assert(false);
return null;
}
this.error(
DiagnosticCode.Type_argument_expected,
range.atEnd
);
}
return null;
}

// otherwise resolve the non-generic call as usual
return this.resolveFunction(prototype, null, new Map(), reportMode);
return resolvedTypeArguments;
}

/** Updates contextual types with a possibly encapsulated inferred type. */
Expand Down Expand Up @@ -3644,15 +3692,34 @@ export class Resolver extends DiagnosticEmitter {

// Resolve type arguments if generic
if (prototype.is(CommonFlags.Generic)) {
resolvedTypeArguments = this.resolveTypeArguments( // reports
assert(prototype.typeParameterNodes), // must be present if generic
typeArgumentNodes,
flow,
ctxElement,
ctxTypes, // update
reportNode,
reportMode
);

// find the constructor prototype, which may be on a base class
let constructorPrototype: FunctionPrototype | null = null;
for (let p: ClassPrototype | null = prototype; p && !constructorPrototype; p = p.basePrototype) {
constructorPrototype = p.constructorPrototype;
}

// if no type arguments are provided, try to infer them from the constructor call
if (!typeArgumentNodes && constructorPrototype && flow && ctxTypes.size == 0) {
resolvedTypeArguments = this.inferGenericTypeArguments(
reportNode as NewExpression,
constructorPrototype,
prototype.typeParameterNodes,
flow,
);
} else {
// resolve them from the provided type argument nodes
resolvedTypeArguments = this.resolveTypeArguments( // reports
assert(prototype.typeParameterNodes), // must be present if generic
typeArgumentNodes,
flow,
ctxElement,
ctxTypes, // update
reportNode,
reportMode
);
}

if (!resolvedTypeArguments) return null;

// Otherwise make sure that no type arguments have been specified
Expand Down
Loading

0 comments on commit 9a7a6e0

Please sign in to comment.