@@ -97,6 +97,10 @@ export function emitWithTsickle(
97
97
afterTsTransformers . push ( ...customTransformers . afterTs ) ;
98
98
}
99
99
}
100
+ afterTsTransformers . push ( ( ctx ) => ( sf ) => {
101
+ addSyntheticCommentsAfterTsTransformer ( ctx , sf ) ;
102
+ return sf ;
103
+ } ) ;
100
104
const writeFileDelegate = writeFile || tsHost . writeFile . bind ( tsHost ) ;
101
105
const modulesManifest = new ModulesManifest ( ) ;
102
106
const writeFileImpl =
@@ -155,10 +159,14 @@ function createTransformer(
155
159
host : TransformerHost ,
156
160
operator : ( sourceFile : ts . SourceFile , sourceMapper : SourceMapper ) =>
157
161
string ) : ts . TransformerFactory < ts . SourceFile > {
158
- return ( context : ts . TransformationContext ) => ( sourceFile : ts . SourceFile ) : ts . SourceFile => {
162
+ return ( context : TsickleTransformationContext ) => ( sourceFile : ts . SourceFile ) : ts . SourceFile => {
159
163
if ( host . shouldSkipTsickleProcessing ( sourceFile . fileName ) ) {
160
164
return sourceFile ;
161
165
}
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 ( ) ;
162
170
const sourceMapper = new NodeSourceMapper ( ) ;
163
171
const newFile = ts . createSourceFile (
164
172
sourceFile . fileName , operator ( sourceFile , sourceMapper ) , ts . ScriptTarget . Latest , true ) ;
@@ -182,7 +190,7 @@ function createTransformer(
182
190
transformedFile . statements = synthStmts ;
183
191
// Need to set parents as some of TypeScript's emit logic relies on it (e.g. emitting
184
192
// decorators)
185
- synthStmts . forEach ( stmt => setParentsForSyntheticNodes ( stmt , transformedFile ) ) ;
193
+ synthStmts . forEach ( stmt => setParentsForSyntheticNodes ( context , stmt , transformedFile ) ) ;
186
194
return transformedFile ;
187
195
} ;
188
196
}
@@ -252,48 +260,18 @@ function convertToSyntheticNode(
252
260
}
253
261
}
254
262
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 ) {
289
264
// Note: Somewhow, TypeScript does not write exports correctly if we
290
265
// have an original node for an export declaration. So we reset it to undefined.
291
266
// This still allows TypeScript to elided exports as it will actually check
292
267
// the values in the exportClause instead.
293
- if ( node . kind === ts . SyntaxKind . ExportDeclaration ) {
294
- ts . setOriginalNode ( node , undefined ) ;
295
- }
268
+ ts . setOriginalNode ( node , originalNode ) ;
296
269
}
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 ;
297
275
}
298
276
return node ;
299
277
}
@@ -480,27 +458,157 @@ class CommentSynthesizer {
480
458
481
459
482
460
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 ) => {
484
463
let commentText = text . substring ( pos , end ) . trim ( ) ;
485
464
if ( kind === ts . SyntaxKind . MultiLineCommentTrivia ) {
486
465
commentText = commentText . replace ( / ( ^ \/ \* ) | ( \* \/ $ ) / g, '' ) ;
487
466
} 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
+ }
488
471
commentText = commentText . replace ( / ( ^ \/ \/ ) / g, '' ) ;
489
472
}
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 } ) ;
493
474
} ) ;
475
+ return synthesizedComments ;
494
476
}
495
477
}
496
478
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 ) ;
500
609
}
501
- node . parent = parent ;
502
610
node . forEachChild ( child => {
503
- setParentsForSyntheticNodes ( child , node ) ;
611
+ setParentsForSyntheticNodes ( ctx , child , node ) ;
504
612
} ) ;
505
613
}
506
614
0 commit comments