Skip to content

Commit c1937cb

Browse files
JounQinthorn0
andauthored
feat: add new parser babel-ts to parse TypeScript syntaxes via Babel (#6400)
* feat: add new parser `babel-ts` to parse TypeScript syntaxes via Babel * docs: document parser babel-ts * chore: enable babel-ts in markdown and worker.js * verify all TS tests against babel-ts except disabled by disableBabelTS * support for TSDeclareMethod; fix abstract properties and methods * fix embedded angular templates and styles * fix TSImportType * simplify babel options combinations * fix method decorators * fix ranges * work around loc inconsistencies for SequenceExpression * don't print trailing comma in type parameters (unsupported in TS < 3.3) * preserve quotes for class properties * align printing of single-parameter functions * fix comments in degenerate single-element unions and intersections * fix comments for methods * fix comments for mapped types * edit docs, change 'since' to 2.0.0 * add changelog * fix code sample on playground * update snapshots after rebase Co-authored-by: Georgii Dolzhykov <[email protected]>
1 parent 0c51368 commit c1937cb

41 files changed

Lines changed: 375 additions & 147 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#### Babel as an alternative parser for TypeScript ([#6400](https://github.com/prettier/prettier/pull/6400) by [@JounQin](https://github.com/JounQin) & [@thorn0](https://github.com/thorn0))
2+
3+
A new value for the `parser` option has been added: `babel-ts`, which stands for Babel with its TypeScript support enabled. The `babel-ts` parser supports JavaScript features not yet supported by TypeScript (ECMAScript proposals, e.g. [private methods and accessors](https://github.com/tc39/proposal-private-methods)), but it's less permissive when it comes to error recovery and less battle-tested than the `typescript` parser. While Babel's TypeScript plugin is quite mature, ASTs produced by the two parsers aren't 100% compatible. We tried to take the discrepancies into account, but there still must remain cases where code gets formatted differently or even incorrectly. We call upon the community to help us find such cases. If you see them, please raise issues. In the long run, this will help with [unifying the AST format](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/typescript-estree#ast-alignment-tests) in future versions of the parsers and thus contribute to a better, more solid JavaScript parser ecosystem.

docs/options.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,13 @@ Specify which parser to use.
196196

197197
Prettier automatically infers the parser from the input file path, so you shouldn't have to change this setting.
198198

199-
Both the `babel` and `flow` parsers support the same set of JavaScript features (including Flow type annotations). They might differ in some edge cases, so if you run into one of those you can try `flow` instead of `babel`.
199+
Both the `babel` and `flow` parsers support the same set of JavaScript features (including Flow type annotations). They might differ in some edge cases, so if you run into one of those you can try `flow` instead of `babel`. Almost the same applies to `typescript` and `babel-ts`. `babel-ts` might support JavaScript features (proposals) not yet supported by TypeScript, but it's less permissive when it comes to invalid code and less battle-tested than the `typescript` parser.
200200

201201
Valid options:
202202

203203
- `"babel"` (via [@babel/parser](https://github.com/babel/babel/tree/master/packages/babel-parser)) _Named `"babylon"` until v1.16.0_
204-
- `"babel-flow"` (Same as `"babel"` but enables Flow parsing explicitly to avoid ambiguity) _First available in v1.16.0_
204+
- `"babel-flow"` (same as `"babel"` but enables Flow parsing explicitly to avoid ambiguity) _First available in v1.16.0_
205+
- `"babel-ts"` (similar to `"typescript"` but uses Babel and its TypeScript plugin) _First available in v2.0.0_
205206
- `"flow"` (via [flow-parser](https://github.com/facebook/flow/tree/master/src/parser))
206207
- `"typescript"` (via [@typescript-eslint/typescript-estree](https://github.com/typescript-eslint/typescript-eslint)) _First available in v1.4.0_
207208
- `"css"` (via [postcss-scss](https://github.com/postcss/postcss-scss) and [postcss-less](https://github.com/shellscape/postcss-less), autodetects which to use) _First available in v1.7.1_

src/common/internal-plugins.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ module.exports = [
1919
"babel-flow"
2020
];
2121
},
22+
get "babel-ts"() {
23+
return eval("require")("../language-js/parser-babylon").parsers[
24+
"babel-ts"
25+
];
26+
},
2227
get babylon() {
2328
return eval("require")("../language-js/parser-babylon").parsers.babel;
2429
},

src/language-js/clean.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function clean(ast, newObj, parent) {
77
"comments",
88
"leadingComments",
99
"trailingComments",
10+
"innerComments",
1011
"extra",
1112
"start",
1213
"end",

src/language-js/comments.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,9 @@ function handleMethodNameComments(
460460
if (
461461
enclosingNode &&
462462
precedingNode &&
463+
// "MethodDefinition" is handled in getCommentChildNodes
463464
(enclosingNode.type === "Property" ||
464-
enclosingNode.type === "MethodDefinition" ||
465+
enclosingNode.type === "TSDeclareMethod" ||
465466
enclosingNode.type === "TSAbstractMethodDefinition") &&
466467
precedingNode.type === "Identifier" &&
467468
enclosingNode.key === precedingNode &&
@@ -487,6 +488,7 @@ function handleMethodNameComments(
487488
enclosingNode.type === "ClassProperty" ||
488489
enclosingNode.type === "TSAbstractClassProperty" ||
489490
enclosingNode.type === "TSAbstractMethodDefinition" ||
491+
enclosingNode.type === "TSDeclareMethod" ||
490492
enclosingNode.type === "MethodDefinition")
491493
) {
492494
addTrailingComment(precedingNode, comment);
@@ -558,7 +560,8 @@ function handleCommentInEmptyParens(text, enclosingNode, comment, options) {
558560
if (
559561
enclosingNode &&
560562
((isRealFunctionLikeNode(enclosingNode) &&
561-
enclosingNode.params.length === 0) ||
563+
// `params` vs `parameters` - see https://github.com/babel/babel/issues/9231
564+
(enclosingNode.params || enclosingNode.parameters).length === 0) ||
562565
((enclosingNode.type === "CallExpression" ||
563566
enclosingNode.type === "OptionalCallExpression" ||
564567
enclosingNode.type === "NewExpression") &&
@@ -623,10 +626,14 @@ function handleLastFunctionArgComments(
623626
followingNode.type === "BlockStatement"
624627
) {
625628
const functionParamRightParenIndex = (() => {
626-
if (enclosingNode.params.length !== 0) {
629+
if ((enclosingNode.params || enclosingNode.parameters).length !== 0) {
627630
return privateUtil.getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
628631
text,
629-
options.locEnd(privateUtil.getLast(enclosingNode.params))
632+
options.locEnd(
633+
privateUtil.getLast(
634+
enclosingNode.params || enclosingNode.parameters
635+
)
636+
)
630637
);
631638
}
632639
const functionParamLeftParenIndex = privateUtil.getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
@@ -899,14 +906,51 @@ function isRealFunctionLikeNode(node) {
899906
node.type === "TSConstructSignatureDeclaration" ||
900907
node.type === "TSMethodSignature" ||
901908
node.type === "TSConstructorType" ||
902-
node.type === "TSFunctionType"
909+
node.type === "TSFunctionType" ||
910+
node.type === "TSDeclareMethod"
903911
);
904912
}
905913

914+
function getGapRegex(enclosingNode) {
915+
if (
916+
enclosingNode &&
917+
enclosingNode.type !== "BinaryExpression" &&
918+
enclosingNode.type !== "LogicalExpression"
919+
) {
920+
// Support degenerate single-element unions and intersections.
921+
// E.g.: `type A = /* 1 */ & B`
922+
return /^[\s(&|]*$/;
923+
}
924+
}
925+
926+
function getCommentChildNodes(node, options) {
927+
// Prevent attaching comments to FunctionExpression in this case:
928+
// class Foo {
929+
// bar() // comment
930+
// {
931+
// baz();
932+
// }
933+
// }
934+
if (
935+
(options.parser === "typescript" || options.parser === "flow") &&
936+
node.type === "MethodDefinition" &&
937+
node.value &&
938+
node.value.type === "FunctionExpression" &&
939+
node.value.params.length === 0 &&
940+
!node.value.returnType &&
941+
(!node.value.typeParameters || node.value.typeParameters.length === 0) &&
942+
node.value.body
943+
) {
944+
return [...(node.decorators || []), node.key, node.value.body];
945+
}
946+
}
947+
906948
module.exports = {
907949
handleOwnLineComment,
908950
handleEndOfLineComment,
909951
handleRemainingComment,
910952
hasLeadingComment,
911-
isBlockComment
953+
isBlockComment,
954+
getGapRegex,
955+
getCommentChildNodes
912956
};

src/language-js/embed.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ function isAngularComponentStyles(path) {
391391
node => node.type === "TemplateLiteral",
392392
(node, name) => node.type === "ArrayExpression" && name === "elements",
393393
(node, name) =>
394-
node.type === "Property" &&
394+
(node.type === "Property" || node.type === "ObjectProperty") &&
395395
node.key.type === "Identifier" &&
396396
node.key.name === "styles" &&
397397
name === "value",
@@ -402,7 +402,7 @@ function isAngularComponentTemplate(path) {
402402
return path.match(
403403
node => node.type === "TemplateLiteral",
404404
(node, name) =>
405-
node.type === "Property" &&
405+
(node.type === "Property" || node.type === "ObjectProperty") &&
406406
node.key.type === "Identifier" &&
407407
node.key.name === "template" &&
408408
name === "value",

src/language-js/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ const languages = [
3232
createLanguage(require("linguist-languages/data/TypeScript"), data => ({
3333
...data,
3434
since: "1.4.0",
35-
parsers: ["typescript"],
35+
parsers: ["typescript", "babel-ts"],
3636
vscodeLanguageIds: ["typescript"]
3737
})),
3838
createLanguage(require("linguist-languages/data/TSX"), data => ({
3939
...data,
4040
since: "1.4.0",
41-
parsers: ["typescript"],
41+
parsers: ["typescript", "babel-ts"],
4242
vscodeLanguageIds: ["typescriptreact"]
4343
})),
4444
createLanguage(require("linguist-languages/data/JSON"), data => ({

src/language-js/loc.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,23 @@ function locEnd(node) {
5959
return loc;
6060
}
6161

62-
function composeLoc(startNode, endNode = startNode) {
63-
const loc = {};
64-
if (typeof startNode.start === "number") {
65-
loc.start = startNode.start;
66-
loc.end = endNode.end;
67-
}
68-
if (Array.isArray(startNode.range)) {
69-
loc.range = [startNode.range[0], endNode.range[1]];
70-
}
71-
loc.loc = {
72-
start: startNode.loc.start,
73-
end: endNode.loc.end
62+
function composeLoc(startNode, endNodeOrLength = startNode) {
63+
const length = typeof endNodeOrLength === "number" ? endNodeOrLength : -1;
64+
const start = locStart(startNode);
65+
const end = length !== -1 ? start + length : locEnd(endNodeOrLength);
66+
const startLoc = startNode.loc.start;
67+
return {
68+
start,
69+
end,
70+
range: [start, end],
71+
loc: {
72+
start: startLoc,
73+
end:
74+
length !== -1
75+
? { line: startLoc.line, column: startLoc.column + length }
76+
: endNodeOrLength.loc.end
77+
}
7478
};
75-
return loc;
7679
}
7780

7881
module.exports = {

src/language-js/parser-babylon.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { hasPragma } = require("./pragma");
88
const locFns = require("./loc");
99
const postprocess = require("./postprocess");
1010

11-
function babelOptions(extraOptions, extraPlugins = []) {
11+
function babelOptions(extraPlugins = []) {
1212
return {
1313
sourceType: "module",
1414
allowAwaitOutsideFunction: true,
@@ -18,7 +18,6 @@ function babelOptions(extraOptions, extraPlugins = []) {
1818
allowUndeclaredExports: true,
1919
errorRecovery: true,
2020
plugins: [
21-
"jsx",
2221
"doExpressions",
2322
"objectRestSpread",
2423
"classProperties",
@@ -41,20 +40,23 @@ function babelOptions(extraOptions, extraPlugins = []) {
4140
"classPrivateMethods",
4241
"v8intrinsic",
4342
"partialApplication",
44-
["decorators", { decoratorsBeforeExport: false }]
45-
].concat(extraPlugins),
46-
...extraOptions
43+
["decorators", { decoratorsBeforeExport: false }],
44+
...extraPlugins
45+
]
4746
};
4847
}
4948

50-
function createParse(parseMethod, extraPlugins) {
49+
function createParse(parseMethod, ...pluginCombinations) {
5150
return (text, parsers, opts) => {
5251
// Inline the require to avoid loading all the JS if we don't use it
5352
const babel = require("@babel/parser");
5453

5554
let ast;
5655
try {
57-
ast = babel[parseMethod](text, babelOptions({}, extraPlugins));
56+
ast = tryCombinations(
57+
options => babel[parseMethod](text, options),
58+
pluginCombinations.map(babelOptions)
59+
);
5860
} catch (error) {
5961
throw createError(
6062
// babel error prints (l:c) with cols that are zero indexed
@@ -73,9 +75,31 @@ function createParse(parseMethod, extraPlugins) {
7375
};
7476
}
7577

76-
const parse = createParse("parse", ["flow"]);
77-
const parseFlow = createParse("parse", [["flow", { all: true, enums: true }]]);
78-
const parseExpression = createParse("parseExpression");
78+
const parse = createParse("parse", ["jsx", "flow"]);
79+
const parseFlow = createParse("parse", [
80+
"jsx",
81+
["flow", { all: true, enums: true }]
82+
]);
83+
const parseTypeScript = createParse(
84+
"parse",
85+
["jsx", "typescript"],
86+
["typescript"]
87+
);
88+
const parseExpression = createParse("parseExpression", ["jsx"]);
89+
90+
function tryCombinations(fn, combinations) {
91+
let error;
92+
for (let i = 0; i < combinations.length; i++) {
93+
try {
94+
return fn(combinations[i]);
95+
} catch (_error) {
96+
if (!error) {
97+
error = _error;
98+
}
99+
}
100+
}
101+
throw error;
102+
}
79103

80104
function parseJson(text, parsers, opts) {
81105
const ast = parseExpression(text, parsers, opts);
@@ -146,13 +170,15 @@ function assertJsonNode(node, parent) {
146170

147171
const babel = { parse, astFormat: "estree", hasPragma, ...locFns };
148172
const babelFlow = { ...babel, parse: parseFlow };
173+
const babelTypeScript = { ...babel, parse: parseTypeScript };
149174
const babelExpression = { ...babel, parse: parseExpression };
150175

151176
// Export as a plugin so we can reuse the same bundle for UMD loading
152177
module.exports = {
153178
parsers: {
154179
babel,
155180
"babel-flow": babelFlow,
181+
"babel-ts": babelTypeScript,
156182
// aliased to keep backwards compatibility
157183
babylon: babel,
158184
json: {

src/language-js/postprocess.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ function postprocess(ast, options) {
3232
return { ...node.types[0], loc: node.loc, range: node.range };
3333
}
3434
break;
35+
case "TSTypeParameter":
36+
// babel-ts
37+
if (typeof node.name === "string") {
38+
node.name = {
39+
type: "Identifier",
40+
name: node.name,
41+
...composeLoc(node, node.name.length)
42+
};
43+
}
44+
break;
45+
case "SequenceExpression":
46+
// Babel (unlike other parsers) includes spaces and comments in the range. Let's unify this.
47+
if (node.end && node.end > getLast(node.expressions).end) {
48+
node.end = getLast(node.expressions).end;
49+
}
50+
break;
3551
}
3652
});
3753

0 commit comments

Comments
 (0)