Skip to content
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

Skip asterisks after newline when parsing JSDoc types #26528

Merged
merged 3 commits into from
Sep 4, 2018

Conversation

tschaub
Copy link
Contributor

@tschaub tschaub commented Aug 18, 2018

This makes it possible to split types across lines in JSDoc comments.

For example:

/** 
 * @returns {{someReallyReallyReallyLongType: number,
 *    withLotsOfProperties: boolean}} The return.
 */

If the approach is acceptable, I'll add tests.

The one failing test is jsDocFunctionSignatures12.ts

  1 failing

  1) fourslash tests
       tests/cases/fourslash/jsDocFunctionSignatures12.ts
         fourslash test jsDocFunctionSignatures12.ts runs correctly:

      AssertionError: At line 6, col 5: quick info text: expected '(parameter) o: {\n    stringProp: string;\n    *           numProp: number;\n}' to equal '(parameter) o: any'
      + expected - actual

      -(parameter) o: {
      -    stringProp: string;
      -    *           numProp: number;
      -}
      +(parameter) o: any
      
      at TestState.verifyQuickInfoString (src/harness/fourslash.ts:1361:20)
      at Verify.quickInfoIs (src/harness/fourslash.ts:4167:24)
      at eval (eval at runCode (src/harness/fourslash.ts:3425:23), <anonymous>:13:8)
      at runCode (src/harness/fourslash.ts:3426:13)
      at runFourSlashTestContent (src/harness/fourslash.ts:3408:9)
      at Object.runFourSlashTest (src/harness/fourslash.ts:3393:9)
      at Context.<anonymous> (src/testRunner/fourslashRunner.ts:54:39)

I haven't looked at quick info yet, but it looks like that will need to ignore * after newline as well.

Fixes #23667.
Fixes #26846.

@tschaub
Copy link
Contributor Author

tschaub commented Aug 18, 2018

Fixing up the printing of the type is not entirely straightforward. It comes down to getTextOfNodeFromSourceText(). Here includeTrivia is false, but skipTrivia doesn't skip * (which is sort of the whole problem). Not sure what to do about that.

@tschaub
Copy link
Contributor Author

tschaub commented Aug 30, 2018

@sandersn or @andy-ms - any opinions on the problem above (skipping newline+asterisk while skipping trivia in getTextOfNodeFromSourceText())? Or thoughts on an alternative solution to skipping newline+asterisk while parsing JSDoc type expressions?

Adding an inJSDoc (or similar) arg to skipTrivia seems like it would make sense (trivia in JSDoc includes * if preceded by newline & whitespace). I'm uncertain if the right context is available at all the call sites or if this type of change would be accepted.

@tschaub
Copy link
Contributor Author

tschaub commented Sep 2, 2018

Ok, turns out it isn't skipTrivia's responsibility, since this just skips trivia at the start of a string.

A minor change in getTextOfNodeFromSourceText can make things work. By checking if the node is a JSDoc type expression node there, asterisks at the start of lines can be stripped. I'll push a new commit that implements this.

@tschaub
Copy link
Contributor Author

tschaub commented Sep 2, 2018

I've updated the commits so this addresses issues with multiline function signatures as well (see #26846).

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 approach might work but I want to double check with the team to decide whether a short-term fix is the right thing. Also I want to run the user tests and see whether leading * types are a problem in real code. I’ll push a commit with the baseline changes if they’re an improvement.


function setInJSDocType(inType: boolean) {
inJSDocType += inType ? 1 : -1;
}
Copy link
Member

Choose a reason for hiding this comment

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

Are there nested calls to setInJDSocType? Or is there another reason that inJSDocType = inType isn’t enough?

Copy link
Member

Choose a reason for hiding this comment

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

I looked for nested calls and there don't seem to be any.

Copy link
Contributor Author

@tschaub tschaub Sep 4, 2018

Choose a reason for hiding this comment

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

I was seeing nesting while parsing a @typedef with a function property. I think the tests should fail if inJSDocType is a boolean. I'll confirm.

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'll add another test, but there are nested calls to parseJSDocType here:

/**
 * @typedef {{foo: function(string)}} SomeType
 */

And more for a function that accepts a function etc.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. It's too bad -- I thought our JSDoc type parsing was simpler than that.

@@ -3087,6 +3087,7 @@ declare namespace ts {
setScriptTarget(scriptTarget: ScriptTarget): void;
setLanguageVariant(variant: LanguageVariant): void;
setTextPos(textPos: number): void;
setInJSDocType(inType: boolean): void;
Copy link
Member

Choose a reason for hiding this comment

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

Can’t remember if the internal comment works in scanner.ts, but if so, please mark setinjsdoctype as internal to revert this API change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 64bbf5e.

*
* @typedef {function(boolean, string,
* number):
* (string|number)} StringOrNumber2
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test that uses closure’s * type in interesting ways? Eg (1) at the beginning of line (2) following an asterisk at beginning of line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In 64bbf5e I added tests that demonstrate that this @typedef is correctly parsed:

/** 
 * @typedef {{
 *   foo:
 *   *,
 *   bar:
 *   *
 * }} Works
 */

And this is not supported:

/** 
   Multiline type expressions in comments without leading * are not supported.
   @typedef {{
     foo:
     *,
     bar:
     *
   }} DoesNotWork
 */

text = text.replace(/(^|\r?\n|\r)\s*\*\s*/g, "$1");
}

return text;
Copy link
Member

Choose a reason for hiding this comment

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

Does this change fix only fourslash tests or does it affect the type baselines as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This does not affect type baselines, only verify.quickInfoIs() in tests/cases/fourslash/jsDocFunctionSignatures12.ts

@@ -490,12 +490,29 @@ namespace ts {
return getTextOfNodeFromSourceText(sourceFile.text, node, includeTrivia);
}

function isJSDocTypeExpressionOrChild(node: Node): boolean {
if (node.kind === SyntaxKind.JSDocTypeExpression) {
Copy link
Member

Choose a reason for hiding this comment

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

This function would be easier to read (for me) as a single boolean expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean like this?

function isJSDocTypeExpressionOrChild(node: Node): boolean {
    return node.kind === SyntaxKind.JSDocTypeExpression || (node.parent && isJSDocTypeExpressionOrChild(node.parent)) || false;
}

Copy link
Member

Choose a reason for hiding this comment

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

Yes, except you don't need the trailing || false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That will return undefined if node.parent is undefined. Will TypeScript accept that as boolean?

Copy link
Member

@sandersn sandersn Sep 4, 2018

Choose a reason for hiding this comment

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

Since we have strictNullChecks on now, yes. Put a !! before (node.parent && ...

Edit: I mean, no, it will not accept it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like there are no complaints from the compiler. I pushed 3b24fba to change this to a single boolean expression.

let text = sourceText.substring(includeTrivia ? node.pos : skipTrivia(sourceText, node.pos), node.end);

if (isJSDocTypeExpressionOrChild(node)) {
// strip space + asterisk at line start
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this will do the wrong thing for the non-leading-asterisk use of the * type.

Copy link
Member

Choose a reason for hiding this comment

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

We discussed this and agreed that it's acceptable to get this wrong as long as there is a test showing how it gets it wrong.

Copy link
Contributor Author

@tschaub tschaub Sep 4, 2018

Choose a reason for hiding this comment

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

In 64bbf5e I added a test that shows how getTextOfNodeFromSourceText works on this (ugly) code:

/**
 * @param {{
 *   stringProp: string,
 *   numProp: number,
 *   boolProp: boolean,
 *   anyProp: *,
 *   anotherAnyProp:
 *   *,
 *   functionProp:
 *   function(string,
 *   *):
 *   *
 * }} o
 */
function f1(o) {
    o;
}

That results in this being (pretty) printed:

(parameter) o: {
    stringProp: string;
    numProp: number;
    boolProp: boolean;
    anyProp: any;
    anotherAnyProp: any;
    functionProp: (arg0: string, arg1: any) => any;
}

Not sure if this covers what you are talking about.

@@ -42,6 +42,7 @@ namespace ts {
setScriptTarget(scriptTarget: ScriptTarget): void;
setLanguageVariant(variant: LanguageVariant): void;
setTextPos(textPos: number): void;
setInJSDocType(inType: boolean): void;
Copy link
Member

Choose a reason for hiding this comment

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

I think this is where you need to add /* @internal */

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I'll try that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 64bbf5e.

@sandersn
Copy link
Member

sandersn commented Sep 4, 2018

All right, we talked about this change and will take it. We could wait until we have time to make the Right Fix so that the scanner (1) correctly skips initial asterisk at a certain indent level (2) correctly handles comment text, including multiline text with consistent indentation. However, I think that change would probably start similarly to this one, and would require ten times more work. I'm not sure when we'll be able to justify it given that the current code doesn't have too many bugs open on it and the refactoring would not produce new functionality.

Please address the comments, except for fixing the ambiguity of leading *, which I think is fine to skip in all cases. But do add tests showing that we always skip it, even when it produces the wrong results.

@sandersn
Copy link
Member

sandersn commented Sep 4, 2018

I ran the user tests on this branch but didn't see as many changes as I expected. Turns out chrome-devtools-frontend only has a few places with leading asterisks inside a type literal.

@tschaub
Copy link
Contributor Author

tschaub commented Sep 4, 2018

Ok, I've updated things and included more tests to demonstrate what works and what doesn't. The tl;dr is this:

/**
 * @typedef {{
 *   stringProp: string,
 *   numProp: number,
 *   boolProp: boolean,
 *   anyProp: *,
 *   anotherAnyProp:
 *   *,
 *   functionProp:
 *   function(string,
 *   *):
 *   *
 * }} ThisAllWorks
 */

/**
   @typedef {{
     stringProp: string,
     numProp: number,
     boolProp: boolean,
     anyProp: *
   }} ThisAlsoWorks
 */

/**
   @typedef {{
     anotherAnyProp:
     *,
     functionProp:
     function(string,
     *):
     *
   }} ThisDoesNotWork
 */

Or, put another way, if you do not use * at the start of multiline comments, you cannot start a line with * to represent the any type.

@sandersn
Copy link
Member

sandersn commented Sep 4, 2018

@tschaub Yep, I didn't see any breaks in chrome-devtools-frontend from this ambiguity, although it's a big codebase so I might have missed something. I think it's a wart that we can live with given the amount of people who (1) always use * to begin their JSDoc lines (2) never use * as a type.

Thanks for contributing this fix. I think it'll help unlock a lot of Javascript code.

@sandersn sandersn merged commit 262ea5b into microsoft:master Sep 4, 2018
@tschaub tschaub deleted the asteriskless branch September 4, 2018 23:45
@tschaub
Copy link
Contributor Author

tschaub commented Sep 4, 2018

Thanks for helping get this in @sandersn!

@SamB
Copy link

SamB commented Aug 1, 2019

Hmm. Would be great to get some documentation about when various multiline constructs actually started to work...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multiline JSDoc function signatures aren't properly parsed Multiline JSDoc types don't skip asterisks
3 participants