-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Issue 19220 function parameter arity #24031
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
Issue 19220 function parameter arity #24031
Conversation
I am currently working on further analyzing available signatures to convey more useful error information in complex cases. |
After further investigating all possible cases of function parameter arity mismatches I determined that adding them to this pull would require too many code changes. It would however be possible and relatively straightword to add those cases as more accurate diagnostics in the future. |
ace037c
to
3998c54
Compare
@@ -20,7 +20,7 @@ verify.currentSignatureParameterCountIs(2); | |||
verify.currentParameterHelpArgumentNameIs("b"); | |||
verify.currentParameterSpanIs("b: boolean"); | |||
|
|||
edit.insert("x, "); | |||
// edit.insert("x, "); // TODO: This block looks like a duplicate of the above code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this test case necessary?
7dd99e8
to
9cf84de
Compare
@sandersn can you please review this change. |
9cf84de
to
10e2dd3
Compare
@sandersn This PR is now up to date with master again. |
@rFlorian Just a brief note. I started looking at the PR. However, I don't believe this is the right solution. I believe that it catches a case (discontinuous overload sets) where the target function is wrong, not the calling code, and so the error message should (1) point the user to the target function (2) be much easier to implement, since it's a rare case. I don't have data to back me up yet, but I'm in the middle of making a version of the compiler that fails whenever it sees a discontinuous overload set, after which I'll run it on Definitely Typed to see whether real authors ever intentionally write them. I'll report back what I find. |
OK, I've got results from the experiment. Turns out I was wrong. The DOM, jquery and node all have discontinuous overload arities, as do many other projects, including Typescript itself. A few of these I looked at are just fussy, but do aid correctness. The others all follow a similar pattern: Sets of parameters get added in logical groups, such as x/y pairs. An example is So I do think this change is necessary. There is still a trade off between smart messages backed by very complex code versus a simple message backed by simple code. I'm not sure where we should end up on that; I'll take a closer look at the code to help me decide. In case anybody wants to replicate the experiment, here is the code: function kaboom(signatures: ReadonlyArray<Signature>) {
if (length(signatures) === 0) return;
let spans = [];
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (const sig of signatures) {
let x = { min: getMinArgumentCount(sig), max: getParameterCount(sig) }
spans.push(x);
min = Math.min(min, x.min);
max = Math.max(max, x.max);
}
for (var i = min; i <= max; i++) {
if (!find(spans, ({min,max}) => min <= i && i <= max)) {
console.log("Couldn't find " + i + " at " + getTextOfNode(signatures[0].declaration!));
}
}
}
// ... early in checkFunctionOrMethodDeclaration ...
const t = getTypeOfSymbol(getSymbolOfNode(node))
const signatures = getSignaturesOfType(t, SignatureKind.Call);
kaboom(signatures); It's slow and doesn't handle rest parameters quite right but is good enough to find lots of examples. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure the implementation can be made more efficient (and somewhat simpler). Also I would like a slightly different wording of the error message.
@@ -26,7 +26,7 @@ tests/cases/conformance/types/rest/genericRestParameters1.ts(31,1): error TS2556 | |||
f1(ns[0], ns[1], true); | |||
f1(...ns, true); // Error, tuple spread only expanded when last | |||
~~~~~~~~~~~~~~~ | |||
!!! error TS2556: Expected 3 arguments, but got 1 or more. | |||
!!! error TS2557: Expected at least 0 arguments, but got 2 or more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR needs to work with the new tuple-spreading code. Both the expected and actual argument count are now wrong.
@@ -5344,6 +5344,7 @@ declare namespace ts { | |||
Rest_signatures_are_incompatible: DiagnosticMessage; | |||
Property_0_is_incompatible_with_rest_element_type: DiagnosticMessage; | |||
A_rest_element_type_must_be_an_array_type: DiagnosticMessage; | |||
No_overload_expects_0_arguments_The_most_likely_overloads_that_match_expect_either_1_arguments_or_at_least_2_arguments: DiagnosticMessage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would reword this like so:
No overload expects 1 arguments, but overloads do exist that expect either 0 or at least 2 arguments.
src/compiler/checker.ts
Outdated
const hasRestParameter = some(signatures, hasEffectiveRestParameter); | ||
const availableArgumentCounts: number[] = []; | ||
forEach(signatures, (sig: Signature) => { | ||
const maxArgs = sig.parameters.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixing the test failures might be as simple as calling the new helpers getMinArgumentCount
and getParameterCount
instead of directly accessing properties.
src/compiler/checker.ts
Outdated
undefined; | ||
diagnostics.add(createDiagnosticForNode(node, <DiagnosticMessage>error, paramRange, argCount)); | ||
} | ||
else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collapse to else if
src/compiler/checker.ts
Outdated
const error = hasRestParameter && hasSpreadArgument ? Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more : | ||
hasRestParameter ? Diagnostics.Expected_at_least_0_arguments_but_got_1 : | ||
hasSpreadArgument ? Diagnostics.Expected_0_arguments_but_got_1_or_more : | ||
undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip the hasSpreadArgument check and just return Expected 0 arguments but got 1 or more
in the else branch of hasRestParameter.
src/compiler/checker.ts
Outdated
@@ -19043,26 +19043,48 @@ namespace ts { | |||
diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments)); | |||
} | |||
else if (args) { | |||
let min = Number.POSITIVE_INFINITY; | |||
let max = Number.NEGATIVE_INFINITY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should revert all the first section of this change and add two more variables. This worked for me in simple tests:
let belowArgCount = Number.NEGATIVE_INFINITY
let aboveArgCount = Number.POSITIVE_INFINITY
for (const sig of signatures) {
minCount = getMinArgumentCount(sig)
maxCount = getParameterCount(sig)
if (minCount < argCount && argCount - minCount < argCount - belowArgCount) belowArgCount = minCount
if (argCount < maxCount && maxCount - argCount < aboveArgCount - argCount) aboveArgCount = maxCount
min = Math.min(min, minCount)
max = Math.max(max, maxCount)
}
(Note that you'll have to calculate argCount earlier.) Then you can use belowArgCount/aboveArgCount in the new error reporting branch you added in the second half of this change.
@@ -26,7 +26,7 @@ tests/cases/conformance/types/rest/genericRestParameters1.ts(31,1): error TS2556 | |||
f1(ns[0], ns[1], true); | |||
f1(...ns, true); // Error, tuple spread only expanded when last | |||
~~~~~~~~~~~~~~~ | |||
!!! error TS2556: Expected 3 arguments, but got 1 or more. | |||
!!! error TS2557: Expected at least 3 arguments, but got 1 or more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These still emit a slightly changed error message. Is the changed behavior in this case wanted and/or acceptable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. It's not desired, because f1
accepts exactly 3 arguments. There must be some way to notice that the rest param is a tuple type (with known length).
src/compiler/diagnosticMessages.json
Outdated
@@ -2056,6 +2056,10 @@ | |||
"category": "Error", | |||
"code": 2574 | |||
}, | |||
"No overload expects {0} arguments, but overloads do exist that expect either {1} or at least {2} arguments.": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added your simplified error message, however I feel like it might hide information from the user in cases where discontinuous overload sets with argument counts less than the current argument count exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, for example, 4 arguments provided to an overload set that has arity [1,2,5,6,7] ? It will report "Overloads do exist that expect either 2 or at least 5 arguments"
Actually, after thinking a bit more, I'd rather go the other direction and say "Overloads do exist that expect either 2 or 5 arguments". It points out the nearest arities and is terse enough to avoid implying that it has enumerated all possible arities.
@sandersn Thank you for the detailed analysis and hints on the PR. I incorporated your suggestions into the PR with slight modifications. I do not fully understand the requirements for diagnostics generation with regards to the new new tuple-spreading code so those might not work fully as intended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test change in genericRestParameters1 shouldn't happen -- I think there is a check that now exists that you can use to find out whether the rest parameter is a tuple type. Maybe look at hasEffectiveRestParameter and its callers?
Plus a few small style changes.
@@ -26,7 +26,7 @@ tests/cases/conformance/types/rest/genericRestParameters1.ts(31,1): error TS2556 | |||
f1(ns[0], ns[1], true); | |||
f1(...ns, true); // Error, tuple spread only expanded when last | |||
~~~~~~~~~~~~~~~ | |||
!!! error TS2556: Expected 3 arguments, but got 1 or more. | |||
!!! error TS2557: Expected at least 3 arguments, but got 1 or more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. It's not desired, because f1
accepts exactly 3 arguments. There must be some way to notice that the rest param is a tuple type (with known length).
src/compiler/checker.ts
Outdated
const hasSpreadArgument = getSpreadArgumentIndex(args) > -1; | ||
const paramCount = hasRestParameter ? min : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please revert lines 19062-19069 ( if (argCount <= ... min;
)
src/compiler/checker.ts
Outdated
diagnostics.add(createDiagnosticForNode(node, error, paramCount, argCount)); | ||
if (argCount <= max && hasSpreadArgument) argCount--; | ||
|
||
const hasRestParameter = some(signatures, (sig: Signature) => sig.hasRestParameter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should still be some(signatures, hasEffectiveRestParameter)
src/compiler/diagnosticMessages.json
Outdated
@@ -2056,6 +2056,10 @@ | |||
"category": "Error", | |||
"code": 2574 | |||
}, | |||
"No overload expects {0} arguments, but overloads do exist that expect either {1} or at least {2} arguments.": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, for example, 4 arguments provided to an overload set that has arity [1,2,5,6,7] ? It will report "Overloads do exist that expect either 2 or at least 5 arguments"
Actually, after thinking a bit more, I'd rather go the other direction and say "Overloads do exist that expect either 2 or 5 arguments". It points out the nearest arities and is terse enough to avoid implying that it has enumerated all possible arities.
e1ebdcf
to
3f8a69d
Compare
When checking for rest parameters, hasEffectiveRestParameter is now properly utilized again. This fixed both cases where tuple unpacking parameters were erroneously assessed as rest parameters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I'll merge after you revert a couple of small formatting changes.
src/compiler/checker.ts
Outdated
Diagnostics.Expected_0_arguments_but_got_1; | ||
diagnostics.add(createDiagnosticForNode(node, error, paramCount, argCount)); | ||
const hasSpreadArgument = getSpreadArgumentIndex(args) > -1; | ||
if (argCount <= max && hasSpreadArgument) argCount--; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert this change too. (I'm not sure whether the linter will let it through anyway.)
src/compiler/checker.ts
Outdated
|
||
if (hasRestParameter || hasSpreadArgument) { | ||
const error = 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert this line-join and put Diagnostics.Expected 0 arguments but got 1 or more
back on its own line.
Sounds good, I believe everything is now formatted as expected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I'll merge after the CI finishes.
Thanks for all your work on this one @rFlorian ! |
Thanks for helping me with the PR @sandersn ! |
Fixes #19220
Further changes could be made to rigorously check all available overload signatures in order to identify cases where emitting possible overloads as a range is applicable and thus respond with more accurate diagnostics.