Skip to content

More effective call arguments/prototype #43882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 107 additions & 80 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28411,7 +28411,7 @@ namespace ts {
/**
* Returns the effective arguments for an expression that works like a function invocation.
*/
function getEffectiveCallArguments(node: CallLikeExpression): readonly Expression[] {
function getEffectiveCallArguments(node: CallLikeExpression, signatures?: readonly Signature[]): readonly Expression[] {
if (node.kind === SyntaxKind.TaggedTemplateExpression) {
const template = node.template;
const args: Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())];
Expand All @@ -28436,14 +28436,62 @@ namespace ts {
for (let i = spreadIndex; i < args.length; i++) {
const arg = args[i];
// We can call checkExpressionCached because spread expressions never have a contextual type.
const spreadType = arg.kind === SyntaxKind.SpreadElement && (flowLoopCount ? checkExpression((<SpreadElement>arg).expression) : checkExpressionCached((<SpreadElement>arg).expression));
if (spreadType && isTupleType(spreadType)) {
forEach(getTypeArguments(spreadType), (t, i) => {
const flags = spreadType.target.elementFlags[i];
const syntheticArg = createSyntheticExpression(arg, flags & ElementFlags.Rest ? createArrayType(t) : t,
!!(flags & ElementFlags.Variable), spreadType.target.labeledElementDeclarations?.[i]);
effectiveArgs.push(syntheticArg);
});
const spreadType = arg.kind === SyntaxKind.SpreadElement &&
(flowLoopCount ? checkExpression((<SpreadElement>arg).expression) : checkExpressionCached((<SpreadElement>arg).expression));
if (spreadType && everyType(spreadType, isTupleType)) {
if (spreadType.flags & TypeFlags.Union) {
// TODO: Don't do this if there's a matching rest parameter that also has a tuple-union type
let tmp = []
const types = (spreadType as UnionType).types
const typess = types.map(getTypeArguments)
const longest = Math.max(...typess.map(ts => ts.length))
for (let j = 0; j < longest; j++) {
// TODO: Having undefined in the type now includes optionality (but maybe should *only* be optional) (and maybe shouldn't be???)
const t = getUnionType(typess.map(ts => ts[j] || undefinedType))
const flags: ElementFlags = types.map(t => (t as TupleTypeReference).target.elementFlags[j] || ElementFlags.Optional).reduce((total,f) => f | total, 0)
// TODO: bail if flags aren't ElementFlags.Fixed (it might be OK to allow a rest at the end, but calculating the 'end' is a draaaaaaaag)
if (flags & ~ElementFlags.Fixed) {
tmp = [arg]
break

}
// TODO: Not sure at all what to do with labels, but there might be some utilities for this already
const syntheticArg = createSyntheticExpression(arg, t);
tmp.push(syntheticArg)
}
effectiveArgs.push(...tmp)
}
else {
forEach(getTypeArguments(spreadType as TupleTypeReference), (t, i) => {
const flags = (spreadType as TupleTypeReference).target.elementFlags[i];
const syntheticArg = createSyntheticExpression(
arg,
flags & ElementFlags.Rest ? createArrayType(t) : t,
!!(flags & ElementFlags.Variable),
(spreadType as TupleTypeReference).target.labeledElementDeclarations?.[i]);
effectiveArgs.push(syntheticArg);
});
}
}
else if (spreadType && signatures?.length === 1 && signatures[0].declaration && isArrayType(spreadType)) {
// convert T[] into [T?, T?, T?, T], for as many as there are parameters after this spread
// TODO: Don't do this if there's a matching rest parameter (checking all signatures[effectiveArgs.length] might do the trick?)
const params = signatures[0].parameters
const last = lastOrUndefined<ParameterDeclaration | JSDocParameterTag>(signatures[0].declaration.parameters);
if (!!last && isRestParameter(last) && i !== params.length - 1) {
const t = getElementTypeOfArrayType(spreadType)!
for (let j = 0; j < params.length - i - 1; j++) {
// TODO: This loosens checking of spreads dramatically without strict null checks
// It assumes that a spread is always long enough
// Test the prototype without strict null checks, but only ship under that flag.
effectiveArgs.push(createSyntheticExpression(arg, getUnionType([t, undefinedType])))
}
// TODO: The -1 doesn't check that an overlong array is still assignable, so push a non-undefined type on the end
effectiveArgs.push(createSyntheticExpression(arg, t))
}
else {
effectiveArgs.push(arg);
}
}
else {
effectiveArgs.push(arg);
Expand Down Expand Up @@ -28559,88 +28607,67 @@ namespace ts {
}

function getArgumentArityError(node: CallLikeExpression, signatures: readonly Signature[], args: readonly Expression[]) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
let belowArgCount = Number.NEGATIVE_INFINITY;
let aboveArgCount = Number.POSITIVE_INFINITY;
const spreadIndex = getSpreadArgumentIndex(args);
if (spreadIndex > -1) {
return createDiagnosticForNode(args[spreadIndex], Diagnostics.A_spread_must_either_have_a_tuple_type_or_be_passed_to_a_rest_parameter);
}
let min = Number.POSITIVE_INFINITY; // smallest parameter count
let max = Number.NEGATIVE_INFINITY; // larger parameter count
let maxBelow = Number.NEGATIVE_INFINITY; // largest parameter count that is smaller than the number of arguments
let minAbove = Number.POSITIVE_INFINITY; // smallest parameter count that is larger than the number of arguments

let argCount = args.length;
let closestSignature: Signature | undefined;
for (const sig of signatures) {
const minCount = getMinArgumentCount(sig);
const maxCount = getParameterCount(sig);
if (minCount < argCount && minCount > belowArgCount) belowArgCount = minCount;
if (argCount < maxCount && maxCount < aboveArgCount) aboveArgCount = maxCount;
if (minCount < min) {
min = minCount;
const minParameter = getMinArgumentCount(sig);
const maxParameter = getParameterCount(sig);
// shortest/longest parameter counts
if (minParameter < min) {
min = minParameter;
closestSignature = sig;
}
max = Math.max(max, maxCount);
}

if (min < argCount && argCount < max) {
return getDiagnosticForCallNode(node, Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, argCount, belowArgCount, aboveArgCount);
max = Math.max(max, maxParameter);
// shortest parameter count *longer than the call*/longest parameter count *shorter than the call*
if (minParameter < args.length && minParameter > maxBelow) maxBelow = minParameter;
if (args.length < maxParameter && maxParameter < minAbove) minAbove = maxParameter;
}

const hasRestParameter = some(signatures, hasEffectiveRestParameter);
const paramRange = hasRestParameter ? min :
min < max ? min + "-" + max :
min;
const hasSpreadArgument = getSpreadArgumentIndex(args) > -1;
if (argCount <= max && hasSpreadArgument) {
argCount--;
}

let spanArray: NodeArray<Node>;
let related: DiagnosticWithLocation | undefined;

const error = hasRestParameter || hasSpreadArgument ?
hasRestParameter && hasSpreadArgument ?
Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more :
hasRestParameter ?
Diagnostics.Expected_at_least_0_arguments_but_got_1 :
Diagnostics.Expected_0_arguments_but_got_1_or_more :
paramRange === 1 && argCount === 0 && isPromiseResolveArityError(node) ?
Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise :
Diagnostics.Expected_0_arguments_but_got_1;

if (closestSignature && getMinArgumentCount(closestSignature) > argCount && closestSignature.declaration) {
const paramDecl = closestSignature.declaration.parameters[closestSignature.thisParameter ? argCount + 1 : argCount];
if (paramDecl) {
related = createDiagnosticForNode(
paramDecl,
isBindingPattern(paramDecl.name) ? Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided :
isRestParameter(paramDecl) ? Diagnostics.Arguments_for_the_rest_parameter_0_were_not_provided : Diagnostics.An_argument_for_0_was_not_provided,
!paramDecl.name ? argCount : !isBindingPattern(paramDecl.name) ? idText(getFirstIdentifier(paramDecl.name)) : undefined
const parameterRange = hasRestParameter ? min
: min < max ? min + "-" + max
: min;
const error = hasRestParameter ? Diagnostics.Expected_at_least_0_arguments_but_got_1
: parameterRange === 1 && args.length === 0 && isPromiseResolveArityError(node) ? Diagnostics.Expected_0_arguments_but_got_1_Did_you_forget_to_include_void_in_your_type_argument_to_Promise
: Diagnostics.Expected_0_arguments_but_got_1;
if (min < args.length && args.length < max) {
// between min and max, but with no matching overload
return getDiagnosticForCallNode(node, Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, args.length, maxBelow, minAbove);
}
else if (args.length < min) {
// too short: put the error span on the call expression, not any of the args
const diagnostic = getDiagnosticForCallNode(node, error, parameterRange, args.length);
const parameter = closestSignature?.declaration?.parameters[closestSignature.thisParameter ? args.length + 1 : args.length];
if (parameter) {
const parameterError = createDiagnosticForNode(
parameter,
isBindingPattern(parameter.name) ? Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided
: isRestParameter(parameter) ? Diagnostics.Arguments_for_the_rest_parameter_0_were_not_provided
: Diagnostics.An_argument_for_0_was_not_provided,
!parameter.name ? args.length : !isBindingPattern(parameter.name) ? idText(getFirstIdentifier(parameter.name)) : undefined
);
return addRelatedInfo(diagnostic, parameterError);
}
}

if (!hasSpreadArgument && argCount < min) {
const diagnostic = getDiagnosticForCallNode(node, error, paramRange, argCount);
return related ? addRelatedInfo(diagnostic, related) : diagnostic;
}

if (hasRestParameter || hasSpreadArgument) {
spanArray = factory.createNodeArray(args);
if (hasSpreadArgument && argCount) {
const nextArg = elementAt(args, getSpreadArgumentIndex(args) + 1) || undefined;
spanArray = factory.createNodeArray(args.slice(max > argCount && nextArg ? args.indexOf(nextArg) : Math.min(max, args.length - 1)));
}
return diagnostic;
}
else {
spanArray = factory.createNodeArray(args.slice(max));
}

const pos = first(spanArray).pos;
let end = last(spanArray).end;
if (end === pos) {
end++;
// too long; error goes on the excess parameters
const errorSpan = factory.createNodeArray(args.slice(max));
const pos = first(errorSpan).pos;
let end = last(errorSpan).end;
if (end === pos) {
end++;
}
setTextRangePosEnd(errorSpan, pos, end);
return createDiagnosticForNodeArray(getSourceFileOfNode(node), errorSpan, error, parameterRange, args.length);
}
setTextRangePosEnd(spanArray, pos, end);
const diagnostic = createDiagnosticForNodeArray(
getSourceFileOfNode(node), spanArray, error, paramRange, argCount);
return related ? addRelatedInfo(diagnostic, related) : diagnostic;
}

function getTypeArgumentArityError(node: Node, signatures: readonly Signature[], typeArguments: NodeArray<TypeNode>) {
Expand Down Expand Up @@ -28698,7 +28725,7 @@ namespace ts {
return resolveErrorCall(node);
}

const args = getEffectiveCallArguments(node);
const args = getEffectiveCallArguments(node, signatures);

// The excludeArgument array contains true for each context sensitive argument (an argument
// is context sensitive it is susceptible to a one-time permanent contextual typing).
Expand Down
8 changes: 2 additions & 6 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@
"'extends' clause already seen.": {
"category": "Error",
"code": 1172
},
},
"'extends' clause must precede 'implements' clause.": {
"category": "Error",
"code": 1173
Expand Down Expand Up @@ -2386,14 +2386,10 @@
"category": "Error",
"code": 2555
},
"Expected {0} arguments, but got {1} or more.": {
"A spread must either have a tuple type or be passed to a rest parameter.": {
"category": "Error",
"code": 2556
},
"Expected at least {0} arguments, but got {1} or more.": {
"category": "Error",
"code": 2557
},
"Expected {0} type arguments, but got {1}.": {
"category": "Error",
"code": 2558
Expand Down
8 changes: 2 additions & 6 deletions tests/baselines/reference/callOverload.errors.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
tests/cases/conformance/expressions/functionCalls/callOverload.ts(7,7): error TS2554: Expected 1 arguments, but got 4.
tests/cases/conformance/expressions/functionCalls/callOverload.ts(8,15): error TS2554: Expected 2 arguments, but got 4.
tests/cases/conformance/expressions/functionCalls/callOverload.ts(10,1): error TS2555: Expected at least 1 arguments, but got 0.
tests/cases/conformance/expressions/functionCalls/callOverload.ts(11,10): error TS2557: Expected at least 1 arguments, but got 0 or more.


==== tests/cases/conformance/expressions/functionCalls/callOverload.ts (4 errors) ====
==== tests/cases/conformance/expressions/functionCalls/callOverload.ts (3 errors) ====
declare function fn(x: any): void;
declare function takeTwo(x: any, y: any): void;
declare function withRest(a: any, ...args: Array<any>): void;
Expand All @@ -22,7 +21,4 @@ tests/cases/conformance/expressions/functionCalls/callOverload.ts(11,10): error
~~~~~~~~~~
!!! error TS2555: Expected at least 1 arguments, but got 0.
!!! related TS6210 tests/cases/conformance/expressions/functionCalls/callOverload.ts:3:27: An argument for 'a' was not provided.
withRest(...n);
~~~~
!!! error TS2557: Expected at least 1 arguments, but got 0 or more.
!!! related TS6210 tests/cases/conformance/expressions/functionCalls/callOverload.ts:3:27: An argument for 'a' was not provided.
withRest(...n);
Loading