Skip to content

Commit 336e3e8

Browse files
authored
Merge pull request #127 from edsrzf/ts-preserve-space
Preserve blank lines in plugins using TypeScript transforms
2 parents f4f095e + 71bb01a commit 336e3e8

File tree

6 files changed

+364
-187
lines changed

6 files changed

+364
-187
lines changed

.eslintrc.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ module.exports = {
7979
"@typescript-eslint/no-empty-function": "off",
8080
"@typescript-eslint/explicit-function-return-type": "off",
8181
"@typescript-eslint/no-var-requires": "off",
82-
"@typescript-eslint/no-use-before-define": "off"
82+
"@typescript-eslint/no-use-before-define": "off",
83+
"no-useless-constructor": "off",
84+
"@typescript-eslint/no-useless-constructor": "error"
8385
}
8486
}

packages/ts-migrate-plugins/src/plugins/add-conversions.ts

+109-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Plugin } from 'ts-migrate-server';
33
import { isDiagnosticWithLinePosition } from '../utils/type-guards';
44
import getTokenAtPosition from './utils/token-pos';
55
import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions';
6+
import UpdateTracker from './utils/update';
67

78
type Options = AnyAliasOptions;
89

@@ -16,20 +17,16 @@ const supportedDiagnostics = new Set([
1617
const addConversionsPlugin: Plugin<Options> = {
1718
name: 'add-conversions',
1819

19-
run({ fileName, sourceFile, text, options, getLanguageService }) {
20+
run({ fileName, sourceFile, options, getLanguageService }) {
2021
// Filter out diagnostics we care about.
2122
const diags = getLanguageService()
2223
.getSemanticDiagnostics(fileName)
2324
.filter(isDiagnosticWithLinePosition)
2425
.filter((diag) => supportedDiagnostics.has(diag.code));
2526

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();
3330
},
3431

3532
validate: validateAnyAliasOptions,
@@ -38,6 +35,7 @@ const addConversionsPlugin: Plugin<Options> = {
3835
export default addConversionsPlugin;
3936

4037
const addConversionsTransformerFactory = (
38+
updates: UpdateTracker,
4139
diags: ts.DiagnosticWithLocation[],
4240
{ anyAlias }: Options,
4341
) => (context: ts.TransformationContext) => {
@@ -70,16 +68,113 @@ const addConversionsTransformerFactory = (
7068
})
7169
.filter((node): node is ts.Expression => node !== null),
7270
);
73-
return ts.visitNode(file, visit);
71+
visit(file);
72+
return file;
7473
};
7574

76-
function visit(origNode: ts.Node): ts.Node {
75+
function visit(origNode: ts.Node): ts.Node | undefined {
7776
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;
8189
}
8290

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+
}
84152
}
85153
};
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+
}

packages/ts-migrate-plugins/src/plugins/jsdoc.ts

+36-105
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
anyAliasProperty,
88
createValidate,
99
} from '../utils/validateOptions';
10+
import UpdateTracker from './utils/update';
1011

1112
type TypeMap = Record<string, TypeOptions>;
1213

@@ -66,46 +67,39 @@ const optionProperties: Properties = {
6667
const jsDocPlugin: Plugin<Options> = {
6768
name: 'jsdoc',
6869

69-
run({ sourceFile, text, options }) {
70-
const result = ts.transform(sourceFile, [jsDocTransformerFactory(options)]);
71-
const newSourceFile = result.transformed[0];
72-
if (newSourceFile === sourceFile) {
73-
return text;
74-
}
75-
const printer = ts.createPrinter();
76-
return printer.printFile(newSourceFile);
70+
run({ sourceFile, options }) {
71+
const updates = new UpdateTracker(sourceFile);
72+
ts.transform(sourceFile, [jsDocTransformerFactory(updates, options)]);
73+
return updates.apply();
7774
},
7875

7976
validate: createValidate(optionProperties),
8077
};
8178

8279
export default jsDocPlugin;
8380

84-
const jsDocTransformerFactory = ({
85-
annotateReturns,
86-
anyAlias,
87-
typeMap: optionsTypeMap,
88-
}: Options) => (context: ts.TransformationContext) => {
81+
const jsDocTransformerFactory = (
82+
updates: UpdateTracker,
83+
{ annotateReturns, anyAlias, typeMap: optionsTypeMap }: Options,
84+
) => (context: ts.TransformationContext) => {
8985
const { factory } = context;
9086
const anyType = anyAlias
9187
? factory.createTypeReferenceNode(anyAlias, undefined)
9288
: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
9389
const typeMap: TypeMap = { ...defaultTypeMap, ...optionsTypeMap };
94-
95-
return (file: ts.SourceFile) => ts.visitNode(file, visit);
96-
97-
function visit(origNode: ts.Node): ts.Node {
98-
const node = ts.visitEachChild(origNode, visit, context);
99-
if (ts.isFunctionLike(node)) {
100-
return visitFunctionLike(node, ts.isClassDeclaration(origNode.parent));
90+
return (file: ts.SourceFile) => {
91+
visit(file);
92+
return file;
93+
};
94+
95+
function visit(origNode: ts.Node): void {
96+
origNode.forEachChild(visit);
97+
if (ts.isFunctionLike(origNode)) {
98+
visitFunctionLike(origNode, ts.isClassDeclaration(origNode.parent));
10199
}
102-
return node;
103100
}
104101

105-
function visitFunctionLike(
106-
node: ts.SignatureDeclaration,
107-
insideClass: boolean,
108-
): ts.SignatureDeclaration {
102+
function visitFunctionLike(node: ts.SignatureDeclaration, insideClass: boolean): void {
109103
const modifiers =
110104
ts.isMethodDeclaration(node) && insideClass
111105
? modifiersFromJSDoc(node, factory)
@@ -117,90 +111,27 @@ const jsDocTransformerFactory = ({
117111
parameters === node.parameters &&
118112
returnType === node.type
119113
) {
120-
return node;
114+
return;
115+
}
116+
117+
const newModifiers = modifiers ? factory.createNodeArray(modifiers) : undefined;
118+
if (newModifiers) {
119+
if (node.modifiers) {
120+
updates.replaceNodes(node.modifiers, newModifiers);
121+
} else {
122+
const pos = node.name!.getStart();
123+
updates.insertNodes(pos, newModifiers);
124+
}
121125
}
122126

123-
const newModifiers = factory.createNodeArray(modifiers);
124127
const newParameters = factory.createNodeArray(parameters);
125-
const newType = returnType;
128+
const addParens =
129+
ts.isArrowFunction(node) && node.getFirstToken()?.kind !== ts.SyntaxKind.OpenParenToken;
130+
updates.replaceNodes(node.parameters, newParameters, addParens);
126131

127-
switch (node.kind) {
128-
case ts.SyntaxKind.FunctionDeclaration:
129-
return factory.updateFunctionDeclaration(
130-
node,
131-
node.decorators,
132-
newModifiers,
133-
node.asteriskToken,
134-
node.name,
135-
node.typeParameters,
136-
newParameters,
137-
newType,
138-
node.body,
139-
);
140-
case ts.SyntaxKind.MethodDeclaration:
141-
return factory.updateMethodDeclaration(
142-
node,
143-
node.decorators,
144-
newModifiers,
145-
node.asteriskToken,
146-
node.name,
147-
node.questionToken,
148-
node.typeParameters,
149-
newParameters,
150-
newType,
151-
node.body,
152-
);
153-
case ts.SyntaxKind.Constructor:
154-
return factory.updateConstructorDeclaration(
155-
node,
156-
node.decorators,
157-
newModifiers,
158-
newParameters,
159-
node.body,
160-
);
161-
case ts.SyntaxKind.GetAccessor:
162-
return factory.updateGetAccessorDeclaration(
163-
node,
164-
node.decorators,
165-
newModifiers,
166-
node.name,
167-
newParameters,
168-
newType,
169-
node.body,
170-
);
171-
case ts.SyntaxKind.SetAccessor:
172-
return factory.updateSetAccessorDeclaration(
173-
node,
174-
node.decorators,
175-
newModifiers,
176-
node.name,
177-
newParameters,
178-
node.body,
179-
);
180-
case ts.SyntaxKind.FunctionExpression:
181-
return factory.updateFunctionExpression(
182-
node,
183-
newModifiers,
184-
node.asteriskToken,
185-
node.name,
186-
node.typeParameters,
187-
newParameters,
188-
newType,
189-
node.body,
190-
);
191-
case ts.SyntaxKind.ArrowFunction:
192-
return factory.updateArrowFunction(
193-
node,
194-
newModifiers,
195-
node.typeParameters,
196-
newParameters,
197-
newType,
198-
node.equalsGreaterThanToken,
199-
node.body,
200-
);
201-
default:
202-
// Should be impossible.
203-
return node;
132+
const newType = returnType;
133+
if (newType) {
134+
updates.addReturnAnnotation(node, newType);
204135
}
205136
}
206137

0 commit comments

Comments
 (0)