-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Conversation
Fixing up the printing of the type is not entirely straightforward. It comes down to |
@sandersn or @andy-ms - any opinions on the problem above (skipping newline+asterisk while skipping trivia in Adding an |
Ok, turns out it isn't A minor change in |
f92be3f
to
f46389c
Compare
I've updated the commits so this addresses issues with multiline function signatures as well (see #26846). |
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 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; | ||
} |
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.
Are there nested calls to setInJDSocType? Or is there another reason that inJSDocType = inType
isn’t enough?
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 looked for nested calls and there don't seem to be any.
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 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.
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'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.
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.
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; |
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.
Can’t remember if the internal comment works in scanner.ts, but if so, please mark setinjsdoctype as internal to revert this API change.
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.
Done in 64bbf5e.
* | ||
* @typedef {function(boolean, string, | ||
* number): | ||
* (string|number)} StringOrNumber2 |
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.
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.
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.
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; |
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.
Does this change fix only fourslash tests or does it affect the type baselines as well?
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 does not affect type baselines, only verify.quickInfoIs()
in tests/cases/fourslash/jsDocFunctionSignatures12.ts
src/compiler/utilities.ts
Outdated
@@ -490,12 +490,29 @@ namespace ts { | |||
return getTextOfNodeFromSourceText(sourceFile.text, node, includeTrivia); | |||
} | |||
|
|||
function isJSDocTypeExpressionOrChild(node: Node): boolean { | |||
if (node.kind === SyntaxKind.JSDocTypeExpression) { |
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 function would be easier to read (for me) as a single boolean expression.
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.
Do you mean like this?
function isJSDocTypeExpressionOrChild(node: Node): boolean {
return node.kind === SyntaxKind.JSDocTypeExpression || (node.parent && isJSDocTypeExpressionOrChild(node.parent)) || false;
}
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.
Yes, except you don't need the trailing || false
.
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.
That will return undefined
if node.parent
is undefined
. Will TypeScript accept that as boolean
?
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.
Since we have strictNullChecks on now, yes. Put a !!
before (node.parent && ...
Edit: I mean, no, it will not accept it.
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 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 |
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.
Seems like this will do the wrong thing for the non-leading-asterisk use of the * type.
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.
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.
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.
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.
src/compiler/scanner.ts
Outdated
@@ -42,6 +42,7 @@ namespace ts { | |||
setScriptTarget(scriptTarget: ScriptTarget): void; | |||
setLanguageVariant(variant: LanguageVariant): void; | |||
setTextPos(textPos: number): void; | |||
setInJSDocType(inType: boolean): void; |
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 this is where you need to add /* @internal */
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.
Thanks. I'll try that.
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.
Added in 64bbf5e.
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 |
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. |
f46389c
to
64bbf5e
Compare
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 |
@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. |
Thanks for helping get this in @sandersn! |
Hmm. Would be great to get some documentation about when various multiline constructs actually started to work... |
This makes it possible to split types across lines in JSDoc comments.
For example:
If the approach is acceptable, I'll add tests.
The one failing test is
jsDocFunctionSignatures12.ts
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.