-
Notifications
You must be signed in to change notification settings - Fork 25.3k
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
feat(compiler): support for singleline, multiline & jsdoc comments #22715
Conversation
@@ -1467,3 +1484,165 @@ export function literal( | |||
value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null): LiteralExpr { | |||
return new LiteralExpr(value, type, sourceSpan); | |||
} | |||
|
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.
all of the code in this file below this line is copy pasted from tsickle
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 we going to add enough JsDoc comments to justify this 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.
I don't know, I can make a simple implementation for now that doesn't check the tags at all and just formats the string, and we'll expand it later if we need to
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 code has been updated to only keep the essentials
export class CommentStmt extends Statement { | ||
constructor(public comment: string, sourceSpan?: ParseSourceSpan|null) { | ||
constructor(public comment: string, sourceSpan?: ParseSourceSpan|null, public multiline = 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.
should I put multiline
before sourceSpan
? I didn't want to break anything since it's exported
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 move it before span. This is exported but nothing in @angular/compiler
nor @angular/compiler-cli
are considered part of the public API are are not under the same restrictions as say @angular/core
.
8e06776
to
f459a23
Compare
const statements: any[] = [].concat( | ||
...stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null)); | ||
const statements: any[] = | ||
[].concat(...stmts.map(stmt => stmt.visitStatement(converter, sourceFile)) |
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.
Consider removing [].concat(...
as it doesn't appear to be doing anything.
export class CommentStmt extends Statement { | ||
constructor(public comment: string, sourceSpan?: ParseSourceSpan|null) { | ||
constructor(public comment: string, sourceSpan?: ParseSourceSpan|null, public multiline = 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.
Please move it before span. This is exported but nothing in @angular/compiler
nor @angular/compiler-cli
are considered part of the public API are are not under the same restrictions as say @angular/core
.
visitCommentStmt(stmt: CommentStmt) { return null; } | ||
visitCommentStmt(stmt: CommentStmt, context: any) { | ||
let comment = stmt.comment; | ||
const commentStmt = ts.createNotEmittedStatement(context); |
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 and JSDocCommentStmt
substantially the same here and also in the AbstractEmitterVisitor
which seems to argue for JSDoc being a field of CommentStmt instead of a separate node.
If we decide to keep the nodes separate then we need to create a helper function that both these methods call.
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.
either one works for me, what do you think @alxhub ?
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.
@ocombe As discussed on slack, keep the JSDocCommentStmt
but create helper methods that contain the shared code in the handlers.
@@ -1467,3 +1484,165 @@ export function literal( | |||
value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null): LiteralExpr { | |||
return new LiteralExpr(value, type, sourceSpan); | |||
} | |||
|
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 we going to add enough JsDoc comments to justify this code?
visitCommentStmt(stmt: CommentStmt) { return null; } | ||
visitCommentStmt(stmt: CommentStmt, context: any) { | ||
let comment = stmt.comment; | ||
const commentStmt = ts.createNotEmittedStatement(context); |
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.
@ocombe As discussed on slack, keep the JSDocCommentStmt
but create helper methods that contain the shared code in the handlers.
Please remove all references to ivy from the commit message and PR header (you can say that this feature will be used by Ivy but it is otherwise unrelated). |
You can preview 40e1ca6 at https://pr22715-40e1ca6.ngbuilds.io/. |
|
||
private createCommentStmt( | ||
text: string, | ||
kind: ts.SyntaxKind.SingleLineCommentTrivia|ts.SyntaxKind.MultiLineCommentTrivia, |
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.
would it make sense for kind
to be singleLine: boolean
instead and the ts impl details stay encapsulated inside this private method ?
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 good idea
private createCommentStmt( | ||
text: string, | ||
kind: ts.SyntaxKind.SingleLineCommentTrivia|ts.SyntaxKind.MultiLineCommentTrivia, | ||
context: any): ts.NotEmittedStatement { |
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.
"context" is meaningless here, please update the name and the type.
(you might also want to update the "context" name and type in visit(.*)CommentStmt()
text: string, | ||
kind: ts.SyntaxKind.SingleLineCommentTrivia|ts.SyntaxKind.MultiLineCommentTrivia, | ||
context: any): ts.NotEmittedStatement { | ||
let commentStmt = ts.createNotEmittedStatement(context); |
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.
const
it('should support multiline comments', () => { | ||
expect(emitStmt(new o.CommentStmt('Multiline comment', true))) | ||
.toBe('/* Multiline comment */'); | ||
expect(emitStmt(new o.CommentStmt(`Multiline\ncomment`, true), 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.
q: what is the "false" here (ie why should the call be different depending on the comment content ?)
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.
Because by default the emitStmt
will remove newlines and such, it's easier to test like this when newlines are irrelevant, especially since TS will add spaces for indentation.
But in this case I wanted to make sure that it was adding new lines, that's why I changed 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.
Format.Raw
and Format.Flat
would be nicer IMO.
(Not asking to change this time, up to you)
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.
Agreed.
.toBe(`/* Multiline\ncomment */`); | ||
}); | ||
|
||
it('should support JSDoc comments', () => { |
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.
Why ? (do we need this ?)
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.
to add i18n desc/meaning/id info for closure
.toBe(`/**\n * @desc description\n */`); | ||
expect(emitStmt( | ||
new o.JSDocCommentStmt([ | ||
{text: 'Intro comment'}, {tagName: o.JSDocTag.Desc, text: 'description'}, |
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.
nit: adding a trailing comma would make the formatted output more readable with 1 obj per 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.
oh nice!
constructor(public tags: Tag[] = [], sourceSpan?: ParseSourceSpan|null) { | ||
super(null, sourceSpan); | ||
} | ||
isEquivalent(stmt: Statement): boolean { return stmt instanceof JSDocCommentStmt; } |
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.
Not sure about the usage of isEquivalent()
but sounds like we should also make sure the tags are equivalent ?
The purpose of those tags is to add metadata.
@chuckjaz
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 purpose of isEquivalent()
is to detect changes that require a new .js
file to be emitted in watch mode. Normally comments are ignored. As these comments are semantic we should re-emit the file if the comment changes so isEquivalent should ensure the tags are equivalent as well as the type.
visitStatement(visitor: StatementVisitor, context: any): any { | ||
return visitor.visitJSDocCommentStmt(this, context); | ||
} | ||
toString(includeStartEnd = false): string { return serialize(this.tags, includeStartEnd); } |
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.
toString()
should not have a parameter, why is that used ?
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.
something that is left from the tsickle code, removing it since we don't use it
40e1ca6
to
e908aff
Compare
@@ -72,8 +72,8 @@ export function updateSourceFile( | |||
classes.map<[string, ClassStmt]>(classStatement => [classStatement.name, classStatement])); | |||
const classNames = new Set(classes.map(classStatement => classStatement.name)); | |||
|
|||
const prefix = | |||
<ts.Statement[]>prefixStatements.map(statement => statement.visitStatement(converter, null)); | |||
const prefix = <ts.Statement[]>prefixStatements.map( |
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.
Would const prefix: ts.Statement[] = ...
works ?
Better readability IMO
@@ -1467,3 +1482,55 @@ export function literal( | |||
value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null): LiteralExpr { | |||
return new LiteralExpr(value, type, sourceSpan); | |||
} | |||
|
|||
/** The list of JSDoc tags that we currently support. Extend it if needed. */ | |||
export enum JSDocTag { |
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.
Why do we need a enum here (instead of using string ?) at least Tag|string
?
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's what @alxhub asked me to use
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.
@alxhub why ?
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.
One reason I could imagine is to document the values... but I don't see comment here ?
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.
@vicb my view is that since JSDoc tags are semantically meaningful, we should have some typechecking around which tags we support generating. Either an enum or a union type of 'tagname'|'othertag'
would accomplish this, but I don't think we should accept any arbitrary string
.
* TypeScript has an API for JSDoc already, but it's not exposed. | ||
* https://github.com/Microsoft/TypeScript/issues/7393 | ||
* For now we create types that are similar to theirs so that migrating | ||
* to their API will be easier. See e.g. ts.JSDocTag and ts.JSDocComment. |
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.
use backquote for code ref ie around "ts.JSDocTag
"
*/ | ||
export interface Tag { | ||
/** | ||
* tagName is e.g. "param" in an @param declaration. |
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.
quote @param
, tagName
and so on (especially important for @param
)
out += ` @${tag.tagName}`; | ||
} | ||
if (tag.text) { | ||
out += ' ' + tag.text.replace(/@/g, '\\@'); |
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.
Why escaping @
and not /*
, */
?
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.
no idea, @evmar wrote this for tsickle
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 don't have context on this code, but at least in tsickle the incoming comment text itself cannot contain /* or */ (because otherwise the code wouldn't have parsed), and the escaping of @ is so the Closure compiler doesn't parse 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.
} | ||
|
||
/** | ||
* Serializes a Tag into a string usable in a comment. |
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.
- quote
Tag
, - "a string usable in a comment." any string is usable in a comment
- Do not use JSDoc for internal function/methods
- "note the whitespace" well I notice several WS, do you mean the leading one ? Maybe
@tagName text
would be a better description (matchingTag
props)
a759e53
to
7f4e112
Compare
7f4e112
to
770cccd
Compare
visitCommentStmt(stmt: CommentStmt) { return null; } | ||
visitCommentStmt(stmt: CommentStmt, sourceFile: ts.SourceFile) { | ||
return this.createCommentStmt( | ||
stmt.multiline ? ` ${stmt.comment} ` : ` ${stmt.comment}`, stmt.multiline, sourceFile); |
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.
nit: extract ```
stmt.multiline ? ${stmt.comment}
: ` ${stmt.comment}`
to a local const.
Whenever the evaluation order is not important it improves readability - plus you can add a meaningful name
super(null, sourceSpan); | ||
} | ||
isEquivalent(stmt: Statement): boolean { return stmt instanceof CommentStmt; } | ||
isEquivalent(stmt: Statement): boolean { | ||
return stmt instanceof CommentStmt && this.comment === stmt.comment && |
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 don't think this change is required only JsDocComments have a semantic value
@@ -21,5 +21,33 @@ import * as o from '../../src/output/output_ast'; | |||
expect(o.collectExternalReferences([stmt])).toEqual([ref1, ref2]); | |||
}); | |||
}); | |||
|
|||
describe('comments', () => { | |||
it('different CommentStmt should not be equivalent', () => { |
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.
why not ?
this should only be true for JSDoc
627482f
to
5320b98
Compare
You can preview 5320b98 at https://pr22715-5320b98.ngbuilds.io/. |
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 !
tap/189092495 (thanks @alxhub) |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
PR Type
What kind of change does this PR introduce?
What is the current behavior?
There's no way to output comments with ngc
What is the new behavior?
You can create singleline, multiline and jsdoc comments.
I've reused some code of tsickle for the jsdoc comments, since it's not exported by tsickle, I just copy pasted it.
Does this PR introduce a breaking change?