@@ -3,6 +3,7 @@ import { Plugin } from 'ts-migrate-server';
3
3
import { isDiagnosticWithLinePosition } from '../utils/type-guards' ;
4
4
import getTokenAtPosition from './utils/token-pos' ;
5
5
import { AnyAliasOptions , validateAnyAliasOptions } from '../utils/validateOptions' ;
6
+ import UpdateTracker from './utils/update' ;
6
7
7
8
type Options = AnyAliasOptions ;
8
9
@@ -16,20 +17,16 @@ const supportedDiagnostics = new Set([
16
17
const addConversionsPlugin : Plugin < Options > = {
17
18
name : 'add-conversions' ,
18
19
19
- run ( { fileName, sourceFile, text , options, getLanguageService } ) {
20
+ run ( { fileName, sourceFile, options, getLanguageService } ) {
20
21
// Filter out diagnostics we care about.
21
22
const diags = getLanguageService ( )
22
23
. getSemanticDiagnostics ( fileName )
23
24
. filter ( isDiagnosticWithLinePosition )
24
25
. filter ( ( diag ) => supportedDiagnostics . has ( diag . code ) ) ;
25
26
26
- const result = ts . transform ( sourceFile , [ addConversionsTransformerFactory ( diags , options ) ] ) ;
27
- const newSourceFile = result . transformed [ 0 ] ;
28
- if ( newSourceFile === sourceFile ) {
29
- return text ;
30
- }
31
- const printer = ts . createPrinter ( ) ;
32
- return printer . printFile ( newSourceFile ) ;
27
+ const updates = new UpdateTracker ( sourceFile ) ;
28
+ ts . transform ( sourceFile , [ addConversionsTransformerFactory ( updates , diags , options ) ] ) ;
29
+ return updates . apply ( ) ;
33
30
} ,
34
31
35
32
validate : validateAnyAliasOptions ,
@@ -38,6 +35,7 @@ const addConversionsPlugin: Plugin<Options> = {
38
35
export default addConversionsPlugin ;
39
36
40
37
const addConversionsTransformerFactory = (
38
+ updates : UpdateTracker ,
41
39
diags : ts . DiagnosticWithLocation [ ] ,
42
40
{ anyAlias } : Options ,
43
41
) => ( context : ts . TransformationContext ) => {
@@ -70,16 +68,113 @@ const addConversionsTransformerFactory = (
70
68
} )
71
69
. filter ( ( node ) : node is ts . Expression => node !== null ) ,
72
70
) ;
73
- return ts . visitNode ( file , visit ) ;
71
+ visit ( file ) ;
72
+ return file ;
74
73
} ;
75
74
76
- function visit ( origNode : ts . Node ) : ts . Node {
75
+ function visit ( origNode : ts . Node ) : ts . Node | undefined {
77
76
const needsConversion = nodesToConvert . has ( origNode ) ;
78
- const node = ts . visitEachChild ( origNode , visit , context ) ;
79
- if ( ! needsConversion ) {
80
- return node ;
77
+ let node = ts . visitEachChild ( origNode , visit , context ) ;
78
+ if ( node === origNode && ! needsConversion ) {
79
+ return origNode ;
80
+ }
81
+
82
+ if ( needsConversion ) {
83
+ node = factory . createAsExpression ( node as ts . Expression , anyType ) ;
84
+ }
85
+
86
+ if ( shouldReplace ( node ) ) {
87
+ replaceNode ( origNode , node ) ;
88
+ return origNode ;
81
89
}
82
90
83
- return factory . createAsExpression ( node as ts . Expression , anyType ) ;
91
+ return node ;
92
+ }
93
+
94
+ // Nodes that have one expression child called "expression".
95
+ type ExpressionChild =
96
+ | ts . DoStatement
97
+ | ts . IfStatement
98
+ | ts . SwitchStatement
99
+ | ts . WithStatement
100
+ | ts . WhileStatement ;
101
+
102
+ /**
103
+ * For nodes that contain both expression and statement children, only
104
+ * replace the direct expression children. The statements have already
105
+ * been replaced at a lower level and replacing them again can produce
106
+ * duplicate statements or invalid syntax.
107
+ */
108
+ function replaceNode ( origNode : ts . Node , newNode : ts . Node ) : void {
109
+ switch ( origNode . kind ) {
110
+ case ts . SyntaxKind . DoStatement :
111
+ case ts . SyntaxKind . IfStatement :
112
+ case ts . SyntaxKind . SwitchStatement :
113
+ case ts . SyntaxKind . WithStatement :
114
+ case ts . SyntaxKind . WhileStatement :
115
+ updates . replaceNode (
116
+ ( origNode as ExpressionChild ) . expression ,
117
+ ( newNode as ExpressionChild ) . expression ,
118
+ ) ;
119
+ break ;
120
+
121
+ case ts . SyntaxKind . ForStatement :
122
+ updates . replaceNode (
123
+ ( origNode as ts . ForStatement ) . initializer ,
124
+ ( newNode as ts . ForStatement ) . initializer ,
125
+ ) ;
126
+ updates . replaceNode (
127
+ ( origNode as ts . ForStatement ) . condition ,
128
+ ( newNode as ts . ForStatement ) . condition ,
129
+ ) ;
130
+ updates . replaceNode (
131
+ ( origNode as ts . ForStatement ) . incrementor ,
132
+ ( newNode as ts . ForStatement ) . incrementor ,
133
+ ) ;
134
+ break ;
135
+
136
+ case ts . SyntaxKind . ForInStatement :
137
+ case ts . SyntaxKind . ForOfStatement :
138
+ updates . replaceNode (
139
+ ( origNode as ts . ForInOrOfStatement ) . expression ,
140
+ ( newNode as ts . ForInOrOfStatement ) . expression ,
141
+ ) ;
142
+ updates . replaceNode (
143
+ ( origNode as ts . ForInOrOfStatement ) . initializer ,
144
+ ( newNode as ts . ForInOrOfStatement ) . initializer ,
145
+ ) ;
146
+ break ;
147
+
148
+ default :
149
+ updates . replaceNode ( origNode , newNode ) ;
150
+ break ;
151
+ }
84
152
}
85
153
} ;
154
+
155
+ /**
156
+ * Determines whether a node is eligible to be replaced.
157
+ *
158
+ * Replacing only the expression may produce invalid syntax due to missing parentheses.
159
+ * There is still some risk of losing whitespace if the expression is contained within
160
+ * an if statement condition or other construct that can contain blocks.
161
+ */
162
+ function shouldReplace ( node : ts . Node ) : boolean {
163
+ if ( isStatement ( node ) ) {
164
+ return true ;
165
+ }
166
+ switch ( node . kind ) {
167
+ case ts . SyntaxKind . CaseClause :
168
+ case ts . SyntaxKind . ClassDeclaration :
169
+ case ts . SyntaxKind . EnumMember :
170
+ case ts . SyntaxKind . HeritageClause :
171
+ case ts . SyntaxKind . SourceFile : // In case we missed any other case.
172
+ return true ;
173
+ default :
174
+ return false ;
175
+ }
176
+ }
177
+
178
+ function isStatement ( node : ts . Node ) : node is ts . Statement {
179
+ return ts . SyntaxKind . FirstStatement <= node . kind && node . kind <= ts . SyntaxKind . LastStatement ;
180
+ }
0 commit comments