Skip to content

JSDoc @type tag optional parameters #48132

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

Merged
merged 7 commits into from
Aug 10, 2022
Merged

JSDoc @type tag optional parameters #48132

merged 7 commits into from
Aug 10, 2022

Conversation

jablko
Copy link
Contributor

@jablko jablko commented Mar 5, 2022

TS obeys JSDoc @type tag signatures, but not parameter optionality. Projects work around this by adding | void to parameters in @type tags, but this isn't strictly correct, and leads to problems later using those values, without Exclude<param, void>. e.g.

🧐 Investigation

The signatures passed to resolveCall() ignore @type tags entirely, so they have the wrong minArgumentCount, so hasCorrectArity() fails. I think they obey @param tags?

Otherwise, the applicability checks obey @type tags, because getSignatureApplicabilityError() calls getTypeAtPosition(), which ... calls getTypeForVariableLikeDeclaration(), which contains:

                if (isInJSFile(declaration)) {
                    const type = getParameterTypeOfTypeTag(func, declaration);
                    if (type) return type;
                }

since #26694 (or #22692?).

Backing up, resolveCallExpression() calls resolveCall() on callSignatures returned by getSignaturesOfType() (with the wrong minArgumentCount).

getSignaturesOfType() calls ... getSignatureFromDeclaration(), which obeys @param tags, but not @type tags?

So maybe the solution is to (roughly) copy the @type tag logic from getTypeForVariableLikeDeclaration() -> getSignatureFromDeclaration()? That's what I've tried to do in this PR.

🔍 Search terms

jsdoc type tag optional parameters signature arity

🕗 Version & regression information

2.8.0-dev.20180320 - 4.7.0-dev.20220305

⏯️ Playground link

Workbench repro

🧑‍💻 Code

// @checkJs
// @filename: input.js
// @errors: 2345

/**
 * @type {(options?: 'optional') => void}
 */
export function plugin(options) {}

plugin("optional"); // ✔️ No errors, as expected
plugin("bogus"); // ✔️ Argument type error, as expected
plugin(); // ❌ Unexpected arity error

🙁 Actual behavior

$ tsc --noEmit --checkJs input.js 
input.js:11:8 - error TS2345: Argument of type '"bogus"' is not assignable to parameter of type '"optional"'.

11 plugin("bogus"); // ✔️ Argument type error, as expected
          ~~~~~~~

input.js:12:1 - error TS2554: Expected 1 arguments, but got 0.

12 plugin(); // ❌ Unexpected arity error
   ~~~~~~~~

  input.js:8:24
    8 export function plugin(options) {}
                             ~~~~~~~
    An argument for 'options' was not provided.


Found 2 errors in the same file, starting at: input.js:11

🙂 Expected behavior

With this PR:

$ tsc --noEmit --checkJs input.js 
input.js:11:8 - error TS2345: Argument of type '"bogus"' is not assignable to parameter of type '"optional"'.

11 plugin("bogus"); // ✔️ Argument type error, as expected
          ~~~~~~~


Found 1 error in input.js:11

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Mar 5, 2022
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this basic approach is good enough, although it would be slightly more accurate to copy over the properties from signature to a fresh signature whose symbol is the function value instead of the @type annotation.

Why are there so many deleted baseline files? After that I think this will be ready to merge.

@@ -21,9 +21,9 @@ tests/cases/conformance/jsdoc/a.js(13,1): error TS2554: Expected 1 arguments, bu
g() // should error
~~~
!!! error TS2554: Expected 1 arguments, but got 0.
!!! related TS6210 tests/cases/conformance/jsdoc/a.js:5:12: An argument for 's' was not provided.
!!! related TS6210 tests/cases/conformance/jsdoc/a.js:4:13: An argument for 's' was not provided.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error span change shows that re-using the signature isn't precisely correct. It's probably close enough, though, and it would take much more code to copy over the properties of the declared signature to the new signature.

@jakebailey
Copy link
Member

This seems okay to fix the bug, but I'm not sure exactly if the changes in the diff are "good" or not. It seems like what's happening is that the declared parameter name (in the JS) is getting totally overridden by the jsdoc in the symbols output, which feels odd. I'm not sure what practical effect is has, though.

@jablko jablko force-pushed the patch-48 branch 3 times, most recently from 111a593 to 7b85698 Compare March 15, 2022 17:17
Copy link
Contributor Author

@jablko jablko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jakebailey Thanks for reviewing!

@@ -12762,10 +12760,8 @@ namespace ts {
}

function getSignatureFromDeclaration(declaration: SignatureDeclaration | JSDocSignature): Signature {
if (isInJSFile(declaration)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed getSignatureOfTypeTag() already calls isInJSFile(), so I dropped that condition from this change, and from the other getSignatureOfTypeTag()/getParameterTypeOfTypeTag() call sites. It's a cosmetic change I'm happy to skip though, if you prefer?

@@ -20,7 +20,7 @@ var sid = s => s + "!";
/** @type {NoReturn} */
var noreturn = obj => void obj.title
>noreturn : NoReturn
>obj => void obj.title : (obj: { e: number; m: number; title: string; }) => any
>obj => void obj.title : (s: { e: number; m: number; title: string; }) => any
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These .types baselines are okay and expected, I think? We're now obeying the @type tag, so the output is, e.g. the NoReturn type, and the NoReturn parameter name is s.

@jablko jablko force-pushed the patch-48 branch 5 times, most recently from 5645d0d to eab90ca Compare March 17, 2022 16:54
@jablko jablko force-pushed the patch-48 branch 2 times, most recently from 5dd9a1b to f12b97f Compare March 17, 2022 20:07
@jablko
Copy link
Contributor Author

jablko commented Mar 19, 2022

I noticed that there's currently an error if an initializer isn't compatible with it's target, and that this PR was suppressing that, by applying @type tags to both the target and the inializer.

  • Excluding isFunctionExpressionOrArrowFunction() and isObjectLiteralMethod() restored the error.
  • There's already a separate check that function and method declarations are @type tag compatible, in checkFunctionOrMethodDeclaration(). That one's unaffected by this PR.
  • I added on to tests/cases/conformance/jsdoc/checkJsdocTypeTag6.ts, to confirm initializer ≠ @type incompatibility is reported.

⏯️ Playground link

Workbench repro.

🧑‍💻 Code

// @checkJs
// @filename: input.js
// @errors: 2322 8030

// Confirm initializers are compatible.
// They can't have more parameters than the target.

/** @type {() => void} */
function func(more) {}

/** @type {() => void} */
const variable = function (more) {};

/** @type {() => void} */
const arrow = (more) => {};

({
  /** @type {() => void} */
  method(more) {},

  /** @type {() => void} */
  prop: function (more) {},

  /** @type {() => void} */
  arrow: (more) => {},
});

📓 TIL

Some notes from exploring the code (not necessarily of benefit to anyone but myself):

  • @type tags are applied to variables and properties in getTypeForVariableLikeDeclaration():
    // Use type from type annotation if one is present
    const declaredType = tryGetTypeFromEffectiveTypeNode(declaration);
  • applied to object literal methods in getTypeOfVariableOrParameterOrPropertyWorker():
    else if (isObjectLiteralMethod(declaration)) {
      type = tryGetTypeFromEffectiveTypeNode(declaration) || checkObjectLiteralMethod(declaration, CheckMode.Normal);
    }
  • Q: Why aren't they analogously applied to functions and methods in getTypeOfFuncClassEnumModule()?
    A: Because, unlike variables, functions can have multiple, merged declarations. It instead creates an anonymous object type:
    const type = createObjectType(ObjectFlags.Anonymous, symbol);
    which is later resolved by resolveAnonymousTypeMembers() -> getSignaturesOfSymbol(), but this was ignoring @type tags.

@typescript-bot typescript-bot added the Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros label Mar 19, 2022
@jablko jablko force-pushed the patch-48 branch 3 times, most recently from efb13ef to 5bb7079 Compare May 7, 2022 21:39
@sandersn
Copy link
Member

Is this ready to review again?

@jablko
Copy link
Contributor Author

jablko commented May 10, 2022

@sandersn Yes please.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good except for a couple of comment suggestions.

@@ -12941,7 +12939,15 @@ namespace ts {
continue;
}
}
result.push(getSignatureFromDeclaration(decl));
// If this is a function or method declaration, get the accurate minArgumentCount from the @type tag, if present.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If this is a function or method declaration, get the accurate minArgumentCount from the @type tag, if present.
// If this is a function or method declaration, get the signature from the @type tag, if present.

Comment on lines 12943 to 12944
// If this is a variable or property declaration, apply the @type tag to it
// (getTypeForVariableLikeDeclaration()), not to the initializer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would delete this. I tried rewording and I got

Suggested change
// If this is a variable or property declaration, apply the @type tag to it
// (getTypeForVariableLikeDeclaration()), not to the initializer.
// other kinds are handled through contextual typing

which is OK, but not that valuable to state explicitly. I'll leave it up to you whether you want to keep this, since I'm probably over-familiar with the code.

Copy link
Contributor Author

@jablko jablko Jun 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sandersn Thanks! I agree, as written the whole comment isn't that valuable ... I guess I was aiming to answer:

  • Q: Why are we doing this for function and method declarations?
    A: For optional parameters.
  • Q: Why are we excluding contextually-typed kinds?
    A: Because they're already handled elsewhere, and not excluding them would suppress compatibility checks.

I've added a commit with an alternative rewording (based on your suggestions), but I'm very open to going with your preference!

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed this until now. It's late enough in the 4.8 beta that I want to hold this for 4.9, so I'll merge it next week.

@sandersn sandersn merged commit 35c6fbf into microsoft:main Aug 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants