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

feat(compiler): support for singleline, multiline & jsdoc comments #22715

Closed
wants to merge 1 commit into from

Conversation

ocombe
Copy link
Contributor

@ocombe ocombe commented Mar 12, 2018

PR Type

What kind of change does this PR introduce?

[x] Feature

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?

[x] No

@ocombe ocombe added action: review The PR is still awaiting reviews from at least one requested reviewer target: major This PR is targeted for the next major release comp: ivy labels Mar 12, 2018
@ocombe ocombe requested review from chuckjaz and alxhub March 12, 2018 15:50
@@ -1467,3 +1484,165 @@ export function literal(
value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null): LiteralExpr {
return new LiteralExpr(value, type, sourceSpan);
}

Copy link
Contributor Author

@ocombe ocombe Mar 12, 2018

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

Copy link
Contributor

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?

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 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

Copy link
Contributor Author

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) {
Copy link
Contributor Author

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

Copy link
Contributor

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.

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))
Copy link
Contributor

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) {
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 ?

Copy link
Contributor

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);
}

Copy link
Contributor

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);
Copy link
Contributor

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.

@vicb
Copy link
Contributor

vicb commented Mar 12, 2018

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).

@mary-poppins
Copy link

You can preview 40e1ca6 at https://pr22715-40e1ca6.ngbuilds.io/.


private createCommentStmt(
text: string,
kind: ts.SyntaxKind.SingleLineCommentTrivia|ts.SyntaxKind.MultiLineCommentTrivia,
Copy link
Contributor

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 ?

Copy link
Contributor Author

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 {
Copy link
Contributor

@vicb vicb Mar 12, 2018

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);
Copy link
Contributor

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))
Copy link
Contributor

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 ?)

Copy link
Contributor Author

@ocombe ocombe Mar 12, 2018

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

Copy link
Contributor

@vicb vicb Mar 12, 2018

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)

Copy link
Member

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', () => {
Copy link
Contributor

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 ?)

Copy link
Contributor Author

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'},
Copy link
Contributor

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

{...},
{...},
{...},

Copy link
Contributor Author

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; }
Copy link
Contributor

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

Copy link
Contributor

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); }
Copy link
Contributor

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 ?

Copy link
Contributor Author

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

@@ -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(
Copy link
Contributor

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 {
Copy link
Contributor

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 ?

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's what @alxhub asked me to use

Copy link
Contributor

Choose a reason for hiding this comment

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

@alxhub why ?

Copy link
Contributor

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 ?

Copy link
Member

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.
Copy link
Contributor

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.
Copy link
Contributor

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, '\\@');
Copy link
Contributor

@vicb vicb Mar 12, 2018

Choose a reason for hiding this comment

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

Why escaping @ and not /*, */ ?

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @evmar.
@ocombe add a test ?

}

/**
* Serializes a Tag into a string usable in a comment.
Copy link
Contributor

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 (matching Tag props)

@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
visitCommentStmt(stmt: CommentStmt) { return null; }
visitCommentStmt(stmt: CommentStmt, sourceFile: ts.SourceFile) {
return this.createCommentStmt(
stmt.multiline ? ` ${stmt.comment} ` : ` ${stmt.comment}`, stmt.multiline, sourceFile);
Copy link
Contributor

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 &&
Copy link
Contributor

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', () => {
Copy link
Contributor

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

@angular angular deleted a comment from mary-poppins Mar 14, 2018
@angular angular deleted a comment from mary-poppins Mar 14, 2018
@mary-poppins
Copy link

You can preview 5320b98 at https://pr22715-5320b98.ngbuilds.io/.

@angular angular deleted a comment from mary-poppins Mar 14, 2018
Copy link
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

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

Thanks !

@vicb vicb added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 14, 2018
@vicb
Copy link
Contributor

vicb commented Mar 14, 2018

tap/189092495 (thanks @alxhub)

@mhevery mhevery closed this in 3b167be Mar 15, 2018
@ocombe ocombe deleted the feat/ivy/comments branch April 10, 2018 11:33
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime cla: yes target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants