Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Commit 5b8f7a0

Browse files
committed
Preserve comments for static fields, exported variables, import and re-export declarations.
1 parent 47d2bef commit 5b8f7a0

File tree

4 files changed

+157
-79
lines changed

4 files changed

+157
-79
lines changed

src/transformer.ts

Lines changed: 156 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export function emitWithTsickle(
9797
afterTsTransformers.push(...customTransformers.afterTs);
9898
}
9999
}
100+
afterTsTransformers.push((ctx) => (sf) => {
101+
addSyntheticCommentsAfterTsTransformer(ctx, sf);
102+
return sf;
103+
});
100104
const writeFileDelegate = writeFile || tsHost.writeFile.bind(tsHost);
101105
const modulesManifest = new ModulesManifest();
102106
const writeFileImpl =
@@ -155,10 +159,14 @@ function createTransformer(
155159
host: TransformerHost,
156160
operator: (sourceFile: ts.SourceFile, sourceMapper: SourceMapper) =>
157161
string): ts.TransformerFactory<ts.SourceFile> {
158-
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
162+
return (context: TsickleTransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
159163
if (host.shouldSkipTsickleProcessing(sourceFile.fileName)) {
160164
return sourceFile;
161165
}
166+
// Reset the fields on the context for each file.
167+
// This assumes that TypeScript transforms file by file.
168+
context.importOrReexportDeclarations = [];
169+
context.syntheticNodeParents = new Map();
162170
const sourceMapper = new NodeSourceMapper();
163171
const newFile = ts.createSourceFile(
164172
sourceFile.fileName, operator(sourceFile, sourceMapper), ts.ScriptTarget.Latest, true);
@@ -182,7 +190,7 @@ function createTransformer(
182190
transformedFile.statements = synthStmts;
183191
// Need to set parents as some of TypeScript's emit logic relies on it (e.g. emitting
184192
// decorators)
185-
synthStmts.forEach(stmt => setParentsForSyntheticNodes(stmt, transformedFile));
193+
synthStmts.forEach(stmt => setParentsForSyntheticNodes(context, stmt, transformedFile));
186194
return transformedFile;
187195
};
188196
}
@@ -252,48 +260,18 @@ function convertToSyntheticNode(
252260
}
253261
}
254262
if (originalNode) {
255-
ts.setOriginalNode(node, originalNode);
256-
ts.setSourceMapRange(node, originalNode);
257-
// Needed so that e.g. `module { ... }` prints the variable statement
258-
// before the closure.
259-
// tslint:disable-next-line:no-any
260-
(node as any).symbol = (originalNode as any).symbol;
261-
}
262-
if (node.kind === ts.SyntaxKind.VariableStatement) {
263-
if (node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
264-
// Note: TypeScript does not emit synthetic comments on exported variable statements,
265-
// so we have to create a fake statement to hold the comments.
266-
// Note: We cannot use the trick via `ts.setTextRange` that we use for export / import
267-
// declarations, as somehow TypeScript would still skip some comments.
268-
const synthComments = ts.getSyntheticLeadingComments(node);
269-
if (synthComments && synthComments.length) {
270-
ts.setSyntheticLeadingComments(node, []);
271-
const commentStmt = createNotEmittedStatement(originalSourceFile);
272-
ts.setSyntheticLeadingComments(commentStmt, synthComments);
273-
return [commentStmt, node];
274-
}
275-
}
276-
}
277-
278-
if (node.kind === ts.SyntaxKind.ExportDeclaration ||
279-
node.kind === ts.SyntaxKind.ImportDeclaration) {
280-
if (originalNode) {
281-
// Note: TypeScript does not emit synthetic comments on import / export declarations.
282-
// As tsickle never modifies comments of export / import declarations,
283-
// we can just set the original text range and let typescript reuse the original comments.
284-
// This is simpler than emitting a fake statement with the synthesized comments,
285-
// as in that case we would need to calculate whether the export / import statement
286-
// will be elided, which is far from trivial (especially for import statements).
287-
ts.setTextRange(node, {pos: originalNode.getFullStart(), end: originalNode.getEnd()});
288-
263+
if (node.kind !== ts.SyntaxKind.ExportDeclaration) {
289264
// Note: Somewhow, TypeScript does not write exports correctly if we
290265
// have an original node for an export declaration. So we reset it to undefined.
291266
// This still allows TypeScript to elided exports as it will actually check
292267
// the values in the exportClause instead.
293-
if (node.kind === ts.SyntaxKind.ExportDeclaration) {
294-
ts.setOriginalNode(node, undefined);
295-
}
268+
ts.setOriginalNode(node, originalNode);
296269
}
270+
ts.setSourceMapRange(node, originalNode);
271+
// Needed so that e.g. `module { ... }` prints the variable statement
272+
// before the closure.
273+
// tslint:disable-next-line:no-any
274+
(node as any).symbol = (originalNode as any).symbol;
297275
}
298276
return node;
299277
}
@@ -480,27 +458,157 @@ class CommentSynthesizer {
480458

481459

482460
private convertCommentRanges(parsedComments: ts.CommentRange[], text: string) {
483-
return parsedComments.map(({kind, pos, end, hasTrailingNewLine}, commentIdx) => {
461+
const synthesizedComments: ts.SynthesizedComment[] = [];
462+
parsedComments.forEach(({kind, pos, end, hasTrailingNewLine}, commentIdx) => {
484463
let commentText = text.substring(pos, end).trim();
485464
if (kind === ts.SyntaxKind.MultiLineCommentTrivia) {
486465
commentText = commentText.replace(/(^\/\*)|(\*\/$)/g, '');
487466
} else if (kind === ts.SyntaxKind.SingleLineCommentTrivia) {
467+
if (commentText.startsWith('///')) {
468+
// tripple-slash comments are typescript specific, ignore them in the output.
469+
return;
470+
}
488471
commentText = commentText.replace(/(^\/\/)/g, '');
489472
}
490-
const comment:
491-
ts.SynthesizedComment = {kind, text: commentText, hasTrailingNewLine, pos: -1, end: -1};
492-
return comment;
473+
synthesizedComments.push({kind, text: commentText, hasTrailingNewLine, pos: -1, end: -1});
493474
});
475+
return synthesizedComments;
494476
}
495477
}
496478

497-
function setParentsForSyntheticNodes(node: ts.Node, parent: ts.Node|undefined) {
498-
if (!(node.flags & ts.NodeFlags.Synthesized)) {
499-
return;
479+
/**
480+
* This fixes places where the TypeScript transformer does not
481+
* emit synthetic comments.
482+
*/
483+
function addSyntheticCommentsAfterTsTransformer(
484+
ctx: TsickleTransformationContext, node: ts.Node, path: ts.Node[] = [],
485+
importOrReexports: {count: number} = {
486+
count: 0
487+
}) {
488+
if (node.kind === ts.SyntaxKind.Identifier) {
489+
const parent1 = getSyntheticParent(ctx, node);
490+
const parent2 = parent1 && getSyntheticParent(ctx, parent1);
491+
const parent3 = parent2 && getSyntheticParent(ctx, parent2);
492+
493+
if (parent1 && parent1.kind === ts.SyntaxKind.PropertyDeclaration &&
494+
tsickle.hasModifierFlag(parent1, ts.ModifierFlags.Static)) {
495+
// TypeScript ignore synthetic comments on static property declarations.
496+
// find the parent ExpressionStatement like MyClass.foo = ...
497+
const expressionStmt = lastNodeWith(
498+
path, (node) => node.kind === ts.SyntaxKind.ExpressionStatement ? true : undefined);
499+
if (expressionStmt) {
500+
ts.setSyntheticLeadingComments(
501+
expressionStmt, ts.getSyntheticLeadingComments(parent1) || []);
502+
}
503+
} else if (
504+
parent3 && parent3.kind === ts.SyntaxKind.VariableStatement &&
505+
tsickle.hasModifierFlag(parent3, ts.ModifierFlags.Export)) {
506+
// TypeScript ignore synthetic comments on exported variables.
507+
// find the parent ExpressionStatement like exports.foo = ...
508+
const expressionStmt = lastNodeWith(
509+
path, (node) => node.kind === ts.SyntaxKind.ExpressionStatement ? true : undefined);
510+
if (expressionStmt) {
511+
ts.setSyntheticLeadingComments(
512+
expressionStmt, ts.getSyntheticLeadingComments(parent3) || []);
513+
}
514+
}
515+
}
516+
// TypeScript ignore synthetic comments on reexport / import statements.
517+
const moduleName = extractModuleNameFromRequireVariableStatement(node);
518+
if (moduleName && ctx.importOrReexportDeclarations) {
519+
const index = importOrReexports.count++;
520+
const originalImportOrReexportNode = ctx.importOrReexportDeclarations[index];
521+
if (originalImportOrReexportNode) {
522+
ts.setSyntheticLeadingComments(
523+
node, ts.getSyntheticLeadingComments(originalImportOrReexportNode) || []);
524+
}
525+
}
526+
node.forEachChild((child) => {
527+
path.push(node);
528+
addSyntheticCommentsAfterTsTransformer(ctx, child, path, importOrReexports);
529+
path.pop();
530+
});
531+
}
532+
533+
function extractModuleNameFromRequireVariableStatement(node: ts.Node): string|null {
534+
if (node.kind !== ts.SyntaxKind.VariableStatement) {
535+
return null;
536+
}
537+
const varStmt = node as ts.VariableStatement;
538+
const decls = varStmt.declarationList.declarations;
539+
let init: ts.Expression|undefined;
540+
if (decls.length !== 1 || !(init = decls[0].initializer) ||
541+
init.kind !== ts.SyntaxKind.CallExpression) {
542+
return null;
543+
}
544+
const callExpr = init as ts.CallExpression;
545+
if (callExpr.expression.kind !== ts.SyntaxKind.Identifier ||
546+
(callExpr.expression as ts.Identifier).text !== 'require' ||
547+
callExpr.arguments.length !== 1) {
548+
return null;
549+
}
550+
const moduleExpr = callExpr.arguments[0];
551+
if (moduleExpr.kind !== ts.SyntaxKind.StringLiteral) {
552+
return null;
553+
}
554+
return (moduleExpr as ts.StringLiteral).text;
555+
}
556+
557+
function lastNodeWith(nodes: ts.Node[], predicate: (node: ts.Node) => boolean | undefined): ts.Node|
558+
null {
559+
for (let i = nodes.length - 1; i >= 0; i--) {
560+
const node = nodes[i];
561+
if (predicate(node) !== undefined) {
562+
return node;
563+
}
564+
}
565+
return null;
566+
}
567+
568+
interface TsickleTransformationContext extends ts.TransformationContext {
569+
syntheticNodeParents?: Map<ts.Node, ts.Node|undefined>;
570+
importOrReexportDeclarations?: Array<ts.Node&{moduleSpecifier: ts.StringLiteral}>;
571+
}
572+
573+
function getSyntheticParent(ctx: TsickleTransformationContext, node: ts.Node): ts.Node|null {
574+
if (!ctx.syntheticNodeParents) {
575+
return null;
576+
}
577+
return ctx.syntheticNodeParents.get(node) || null;
578+
}
579+
580+
function setParentsForSyntheticNodes(
581+
ctx: TsickleTransformationContext, node: ts.Node, parent: ts.Node|undefined) {
582+
if (node.flags & ts.NodeFlags.Synthesized) {
583+
// Note: don't update the `parent` of original nodes, as:
584+
// 1) we don't want to change them at all
585+
// 2) TS emit becomes errorneous in some cases if we add a synthetic parent.
586+
node.parent = parent;
587+
}
588+
// This field has been filled in our transformer as first step.
589+
ctx.syntheticNodeParents!.set(node, parent);
590+
let importOrReexportNode: ts.Node&{moduleSpecifier: ts.StringLiteral}|undefined;
591+
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
592+
const id = node as ts.ImportDeclaration;
593+
// this should always be the case...
594+
if (id.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) {
595+
importOrReexportNode = node as ts.Node & {moduleSpecifier: ts.StringLiteral};
596+
}
597+
} else if (node.kind === ts.SyntaxKind.ExportDeclaration) {
598+
const ed = node as ts.ExportDeclaration;
599+
// this should always be the case...
600+
if (ed.moduleSpecifier && ed.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) {
601+
// Use any as TypeScript gives a cast error...
602+
// tslint:disable-next-line:no-any
603+
importOrReexportNode = ed as any /* ts.Node & {moduleSpecifier: ts.StringLiteral} */;
604+
}
605+
}
606+
if (importOrReexportNode) {
607+
// This field has been filled in our transformer as first step.
608+
ctx.importOrReexportDeclarations!.push(importOrReexportNode);
500609
}
501-
node.parent = parent;
502610
node.forEachChild(child => {
503-
setParentsForSyntheticNodes(child, node);
611+
setParentsForSyntheticNodes(ctx, child, node);
504612
});
505613
}
506614

src/tsickle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function formatDiagnostics(diags: ts.Diagnostic[]): string {
138138
}
139139

140140
/** @return true if node has the specified modifier flag set. */
141-
function hasModifierFlag(node: ts.Node, flag: ts.ModifierFlags): boolean {
141+
export function hasModifierFlag(node: ts.Node, flag: ts.ModifierFlags): boolean {
142142
return (ts.getCombinedModifierFlags(node) & flag) !== 0;
143143
}
144144

test_files/decorator/decorator.js.transform.patch

Lines changed: 0 additions & 14 deletions
This file was deleted.

test_files/jsdoc/jsdoc.js.transform.patch

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)