From 36bec87d175685400253933b021918af6c9d3f6b Mon Sep 17 00:00:00 2001 From: Alex Rattray Date: Tue, 11 Apr 2017 13:50:47 -0700 Subject: [PATCH] Enable no-semi mode and protect against ASI failures (#1129) --- README.md | 6 +- src/options.js | 3 +- src/printer.js | 230 ++++- .../no-semi/__snapshots__/jsfmt.spec.js.snap | 965 ++++++++++++++++++ tests/no-semi/jsfmt.spec.js | 3 + tests/no-semi/no-semi.js | 152 +++ 6 files changed, 1308 insertions(+), 51 deletions(-) create mode 100644 tests/no-semi/__snapshots__/jsfmt.spec.js.snap create mode 100644 tests/no-semi/jsfmt.spec.js create mode 100644 tests/no-semi/no-semi.js diff --git a/README.md b/README.md index 2fda3946..807b4fb8 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,11 @@ prettier.format(source, { jsxBracketSameLine: false, // Which parser to use. Valid options are 'flow' and 'babylon' - parser: 'babylon' + parser: 'babylon', + + // Whether to add a semicolon at the end of every line (semi: true), + // or only at the beginning of lines that may introduce ASI failures (semi: false) + semi: true }); ``` diff --git a/src/options.js b/src/options.js index 4b6c4d58..9abd7a46 100644 --- a/src/options.js +++ b/src/options.js @@ -11,7 +11,8 @@ var defaults = { trailingComma: "none", bracketSpacing: true, jsxBracketSameLine: false, - parser: "babylon" + parser: "babylon", + semi: true }; var exampleConfig = Object.assign({}, defaults, { diff --git a/src/printer.js b/src/printer.js index 513c95bb..e9e056a7 100644 --- a/src/printer.js +++ b/src/printer.js @@ -109,6 +109,14 @@ function genericPrint(path, options, printPath, args) { needsParens = path.needsParens(); } + if (node.type) { + // HACK: ASI prevention in no-semi mode relies on knowledge of whether + // or not a paren has been inserted (see `exprNeedsASIProtection()`). + // For now, we're just passing that information by mutating the AST here, + // but it would be nice to find a cleaner way to do this. + node.needsParens = needsParens; + } + if (needsParens) { parts.unshift("("); } @@ -124,6 +132,9 @@ function genericPrint(path, options, printPath, args) { function genericPrintNoParens(path, options, print, args) { var n = path.getValue(); + const semi = options.semi + ? ";" + : ""; if (!n) { return ""; @@ -146,7 +157,7 @@ function genericPrintNoParens(path, options, print, args) { if (n.directives) { path.each( function(childPath) { - parts.push(print(childPath), ";", hardline); + parts.push(print(childPath), semi, hardline); if ( util.isNextLineEmpty(options.originalText, childPath.getValue()) ) { @@ -181,7 +192,7 @@ function genericPrintNoParens(path, options, print, args) { case "EmptyStatement": return ""; case "ExpressionStatement": - return concat([path.call(print, "expression"), ";"]); // Babel extension. + return concat([path.call(print, "expression"), semi]); // Babel extension. case "ParenthesizedExpression": return concat(["(", path.call(print, "expression"), ")"]); case "AssignmentExpression": @@ -295,17 +306,7 @@ function genericPrintNoParens(path, options, print, args) { parts.push(path.call(print, "typeParameters")); } - if ( - n.params.length === 1 && - !n.rest && - n.params[0].type === "Identifier" && - !n.params[0].typeAnnotation && - !n.params[0].leadingComments && - !n.params[0].trailingComments && - !n.params[0].optional && - !n.predicate && - !n.returnType - ) { + if (canPrintParamsWithoutParens(n)) { parts.push(path.call(print, "params", 0)); } else { parts.push( @@ -461,7 +462,7 @@ function genericPrintNoParens(path, options, print, args) { parts.push(" as ", path.call(print, "exported")); } - parts.push(" from ", path.call(print, "source"), ";"); + parts.push(" from ", path.call(print, "source"), semi); return concat(parts); case "ExportNamespaceSpecifier": @@ -526,7 +527,7 @@ function genericPrintNoParens(path, options, print, args) { parts.push("{} from "); } - fromParts.push(path.call(print, "source"), ";"); + fromParts.push(path.call(print, "source"), semi); // If there's a very long import, break the following way: // @@ -581,7 +582,7 @@ function genericPrintNoParens(path, options, print, args) { function(childPath) { parts.push( indent( - concat([hardline, print(childPath), ";"]) + concat([hardline, print(childPath), semi]) ) ); }, @@ -647,7 +648,7 @@ function genericPrintNoParens(path, options, print, args) { ); } - parts.push(";"); + parts.push(semi); return concat(parts); case "CallExpression": { @@ -986,7 +987,7 @@ function genericPrintNoParens(path, options, print, args) { namedTypes.ForAwaitStatement.check(parentNode)); if (!(isParentForLoop && parentNode.body !== n)) { - parts.push(";"); + parts.push(semi); } return group(concat(parts)); @@ -1132,9 +1133,9 @@ function genericPrintNoParens(path, options, print, args) { const hasBraces = isCurlyBracket(clause); if (hasBraces) parts.push(" while"); - else parts.push(concat([line, "while"])); + else parts.push(concat([hardline, "while"])); - parts.push(" (", path.call(print, "test"), ");"); + parts.push(" (", path.call(print, "test"), ")", semi); return concat(parts); case "DoExpression": @@ -1144,7 +1145,7 @@ function genericPrintNoParens(path, options, print, args) { if (n.label) parts.push(" ", path.call(print, "label")); - parts.push(";"); + parts.push(semi); return concat(parts); case "ContinueStatement": @@ -1152,7 +1153,7 @@ function genericPrintNoParens(path, options, print, args) { if (n.label) parts.push(" ", path.call(print, "label")); - parts.push(";"); + parts.push(semi); return concat(parts); case "LabeledStatement": @@ -1195,7 +1196,7 @@ function genericPrintNoParens(path, options, print, args) { return concat(parts); case "ThrowStatement": - return concat(["throw ", path.call(print, "argument"), ";"]); + return concat(["throw ", path.call(print, "argument"), semi]); // Note: ignoring n.lexical because it has no printing consequences. case "SwitchStatement": return concat([ @@ -1238,7 +1239,7 @@ function genericPrintNoParens(path, options, print, args) { return concat(parts); // JSX extensions below. case "DebuggerStatement": - return "debugger;"; + return concat(["debugger", semi]); case "JSXAttribute": parts.push(path.call(print, "name")); @@ -1396,7 +1397,7 @@ function genericPrintNoParens(path, options, print, args) { case "ClassPropertyDefinition": parts.push("static ", path.call(print, "definition")); - if (!namedTypes.MethodDefinition.check(n.definition)) parts.push(";"); + if (!namedTypes.MethodDefinition.check(n.definition)) parts.push(semi); return concat(parts); case "ClassProperty": @@ -1428,7 +1429,7 @@ function genericPrintNoParens(path, options, print, args) { if (n.value) parts.push(" = ", path.call(print, "value")); - parts.push(";"); + parts.push(semi); return concat(parts); case "ClassDeclaration": @@ -1572,7 +1573,7 @@ function genericPrintNoParens(path, options, print, args) { path.call(print, "id"), n.predicate ? " " : "", path.call(print, "predicate"), - ";" + semi ]); case "DeclareModule": return printFlowDeclaration(path, [ @@ -1585,10 +1586,10 @@ function genericPrintNoParens(path, options, print, args) { return printFlowDeclaration(path, [ "module.exports", path.call(print, "typeAnnotation"), - ";" + semi ]); case "DeclareVariable": - return printFlowDeclaration(path, ["var ", path.call(print, "id"), ";"]); + return printFlowDeclaration(path, ["var ", path.call(print, "id"), semi]); case "DeclareExportAllDeclaration": return concat(["declare export * from ", path.call(print, "source")]); case "DeclareExportDeclaration": @@ -1828,7 +1829,7 @@ function genericPrintNoParens(path, options, print, args) { concat([hardline, path.call(print, "right")]) ) : concat([" ", path.call(print, "right")]), - ";" + semi ); return concat(parts); @@ -1998,7 +1999,10 @@ function genericPrintNoParens(path, options, print, args) { function printStatementSequence(path, options, print) { let printed = []; - path.map(stmtPath => { + const bodyNode = path.getNode(); + const isClass = bodyNode.type === "ClassBody"; + + path.map((stmtPath, i) => { var stmt = stmtPath.getValue(); // Just in case the AST has been modified to contain falsy @@ -2017,8 +2021,24 @@ function printStatementSequence(path, options, print) { const text = options.originalText; const parts = []; + // in no-semi mode, prepend statement with semicolon if it might break ASI + if (!options.semi && !isClass && stmtNeedsASIProtection(stmtPath)) { + parts.push(";"); + } + parts.push(stmtPrinted); + if (!options.semi && isClass) { + if (classPropMayCauseASIProblems(stmtPath)) { + parts.push(";"); + } else if (stmt.type === "ClassProperty") { + const nextChild = bodyNode.body[i + 1]; + if (classChildNeedsASIProtection(nextChild)) { + parts.push(";"); + } + } + } + if (util.isNextLineEmpty(text, stmt) && !isLastStatement(stmtPath)) { parts.push(hardline); } @@ -2324,6 +2344,20 @@ function printFunctionParams(path, print, options) { ]); } +function canPrintParamsWithoutParens(node) { + return ( + node.params.length === 1 && + !node.rest && + node.params[0].type === "Identifier" && + !node.params[0].typeAnnotation && + !node.params[0].leadingComments && + !node.params[0].trailingComments && + !node.params[0].optional && + !node.predicate && + !node.returnType + ); +} + function printFunctionDeclaration(path, print, options) { var n = path.getValue(); var parts = []; @@ -2414,6 +2448,7 @@ function typeIsFunction(type) { function printExportDeclaration(path, options, print) { const decl = path.getValue(); + const semi = options.semi ? ";" : ""; let parts = ["export "]; namedTypes.Declaration.assert(decl); @@ -2434,7 +2469,7 @@ function printExportDeclaration(path, options, print) { (decl.declaration.type !== "ClassDeclaration" && decl.declaration.type !== "FunctionDeclaration") ) { - parts.push(";"); + parts.push(semi); } } else { if (decl.specifiers && decl.specifiers.length > 0) { @@ -2496,7 +2531,7 @@ function printExportDeclaration(path, options, print) { parts.push(" from ", path.call(print, "source")); } - parts.push(";"); + parts.push(semi); } return concat(parts); @@ -3346,6 +3381,118 @@ function hasLeadingOwnLineComment(text, node) { return res; } +function hasNakedLeftSide(node) { + return ( + node.type === "AssignmentExpression" || + node.type === "BinaryExpression" || + node.type === "LogicalExpression" || + node.type === "ConditionalExpression" || + node.type === "CallExpression" || + node.type === "MemberExpression" || + node.type === "SequenceExpression" || + node.type === "TaggedTemplateExpression" + ); +} + +function getLeftSide(node) { + if (node.expressions) { + return node.expressions[0]; + } + return node.left || node.test || node.callee || node.object || node.tag; +} + +function exprNeedsASIProtection(node) { + // HACK: node.needsParens is added in `genericPrint()` for the sole purpose + // of being used here. It'd be preferable to find a cleaner way to do this. + const maybeASIProblem = node.needsParens || + node.type === "ParenthesizedExpression" || + node.type === "TypeCastExpression" || + (node.type === "ArrowFunctionExpression" && + !canPrintParamsWithoutParens(node)) || + node.type === "ArrayExpression" || + node.type === "ArrayPattern" || + (node.type === "UnaryExpression" && + node.prefix && + (node.operator === "+" || node.operator === "-")) || + node.type === "TemplateLiteral" || + node.type === "TemplateElement" || + node.type === "JSXElement" || + node.type === "RegExpLiteral" || + (node.type === "Literal" && node.pattern) || + (node.type === "Literal" && node.regex); + + if (maybeASIProblem) { + return true; + } + + if (!hasNakedLeftSide(node)) { + return false; + } + + return exprNeedsASIProtection(getLeftSide(node)); +} + +function stmtNeedsASIProtection(path) { + if (!path) return false; + const node = path.getNode(); + + if (node.type !== "ExpressionStatement") { + return false; + } + + return exprNeedsASIProtection(node.expression); +} + +function classPropMayCauseASIProblems(path) { + const node = path.getNode(); + + if (node.type !== "ClassProperty") { + return false; + } + + const name = node.key && node.key.name; + if (!name) { + return false; + } + + // this isn't actually possible yet with most parsers available today + // so isn't properly tested yet. + if (name === "static" || name === "get" || name === "set") { + return true; + } +} + +function classChildNeedsASIProtection(node) { + if (!node) return; + + let isAsync, isGenerator; + switch (node.type) { + case "ClassProperty": + return node.computed; + + // flow + case "MethodDefinition": + // babylon + case "ClassMethod": { + const isAsync = node.value + ? node.value.async + : node.async; + const isGenerator = node.value + ? node.value.generator + : node.generator; + if (isAsync || node.static || node.kind === "get" || node.kind === "set") { + return false; + } + if (node.computed || isGenerator) { + return true; + } + } + + default: + return false; + } +} + // This recurses the return argument, looking for the first token // (the leftmost leaf node) and, if it (or its parents) has any // leadingComments, returns true (so it can be wrapped in parens). @@ -3354,22 +3501,7 @@ function returnArgumentHasLeadingComment(options, argument) { return true; } - const hasCommentableLeftSide = argument.type === "BinaryExpression" || - argument.type === "LogicalExpression" || - argument.type === "ConditionalExpression" || - argument.type === "CallExpression" || - argument.type === "MemberExpression" || - argument.type === "SequenceExpression" || - argument.type === "TaggedTemplateExpression"; - - if (hasCommentableLeftSide) { - const getLeftSide = (node) => { - if (node.expressions) { - return node.expressions[0]; - } - return node.left || node.test || node.callee || node.object || node.tag; - } - + if (hasNakedLeftSide(argument)) { let leftMost = argument; let newLeftMost; while (newLeftMost = getLeftSide(leftMost)) { diff --git a/tests/no-semi/__snapshots__/jsfmt.spec.js.snap b/tests/no-semi/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..11038a7f --- /dev/null +++ b/tests/no-semi/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,965 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`no-semi.js 1`] = ` +" +// with preexisting semi + +x; [1, 2, 3].forEach(fn) +x; [a, b, ...c] = [1, 2] +x; /r/i.test('r') +x; +1 +x; - 1 +x; ('h' + 'i').repeat(10) +x; (1, 2) +x; (() => {})() +x; ({ a: 1 }).entries() +x; ({ a: 1 }).entries() +x; +x; \`string\` +x; (x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} [1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b](){} + + c = 0; + *d(){} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + + q() {}; + [h](){} + + p() {}; + *i(){} + + a = 1; + get ['y']() {} + + a = 1; + static ['y']() {} + + a = 1; + set ['z'](z) {} + + a = 1; + async ['a']() {} + + a = 1; + async *g() {} + + a = 0; + b = 1; +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x; (() => {})() +} + +// flow + +(x: void); +(y: void) + +// check statement clauses + +do break; while (false) +if (true) do break; while (false) + +if (true) 1; else 2 +for (;;) ; +for (x of y) ; + +debugger + +// check that it doesn't break non-ASI + +1 +- 1 + +1 ++ 1 + +1 +/ 1 + +arr +[0] + +fn +(x) + +!1 + +1 +< 1 + +tag +\`string\` + +x; x => x + +while (false) + (function(){}()) + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// with preexisting semi + +x; +[1, 2, 3].forEach(fn); +x; +[a, b, ...c] = [1, 2]; +x; +/r/i.test(\\"r\\"); +x; ++1; +x; +-1; +x; +(\\"h\\" + \\"i\\").repeat(10); +x; +1, 2; +x; +(() => {})(); +x; +({ a: 1 }.entries()); +x; +({ a: 1 }.entries()); +x; +; +x; +\`string\`; +x; +(x, y) => x; + +// doesn't have to be preceded by a semicolon + +class X {} +[1, 2, 3].forEach(fn); + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + +// don't semicolon if it doesn't start statement + +if (true) (() => {})(); + +class A { + a = 0; + [b]() {} + + c = 0; + *d() {} + + e = 0; + [f] = 0; + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + q() {} + [h]() {} + + p() {} + *i() {} + + a = 1; + get [\\"y\\"]() {} + + a = 1; + static [\\"y\\"]() {} + + a = 1; + set [\\"z\\"](z) {} + + a = 1; + async [\\"a\\"]() {} + + a = 1; + async *g() {} + + a = 0; + b = 1; +} + +// being first/last shouldn't break things +class G { + x = 1; +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1; +} + +// check indentation + +if (true) { + x; + (() => {})(); +} + +// flow + +(x: void); +(y: void); + +// check statement clauses + +do + break; +while (false); +if (true) + do + break; + while (false); + +if (true) 1; +else 2; +for (;;); +for (x of y); + +debugger; + +// check that it doesn't break non-ASI + +1 - 1; + +1 + 1; + +1 / 1; + +arr[0]; + +fn(x); + +!1; + +1 < 1; + +tag\`string\`; + +x; +x => x; + +while (false) + (function() {})(); + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c); +" +`; + +exports[`no-semi.js 2`] = ` +" +// with preexisting semi + +x; [1, 2, 3].forEach(fn) +x; [a, b, ...c] = [1, 2] +x; /r/i.test('r') +x; +1 +x; - 1 +x; ('h' + 'i').repeat(10) +x; (1, 2) +x; (() => {})() +x; ({ a: 1 }).entries() +x; ({ a: 1 }).entries() +x; +x; \`string\` +x; (x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} [1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b](){} + + c = 0; + *d(){} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + + q() {}; + [h](){} + + p() {}; + *i(){} + + a = 1; + get ['y']() {} + + a = 1; + static ['y']() {} + + a = 1; + set ['z'](z) {} + + a = 1; + async ['a']() {} + + a = 1; + async *g() {} + + a = 0; + b = 1; +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x; (() => {})() +} + +// flow + +(x: void); +(y: void) + +// check statement clauses + +do break; while (false) +if (true) do break; while (false) + +if (true) 1; else 2 +for (;;) ; +for (x of y) ; + +debugger + +// check that it doesn't break non-ASI + +1 +- 1 + +1 ++ 1 + +1 +/ 1 + +arr +[0] + +fn +(x) + +!1 + +1 +< 1 + +tag +\`string\` + +x; x => x + +while (false) + (function(){}()) + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// with preexisting semi + +x +;[1, 2, 3].forEach(fn) +x +;[a, b, ...c] = [1, 2] +x +;/r/i.test(\\"r\\") +x +;+1 +x +;-1 +x +;(\\"h\\" + \\"i\\").repeat(10) +x +1, 2 +x +;(() => {})() +x +;({ a: 1 }.entries()) +x +;({ a: 1 }.entries()) +x +; +x +;\`string\` +x +;(x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} +;[1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b]() {} + + c = 0; + *d() {} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + q() {} + [h]() {} + + p() {} + *i() {} + + a = 1 + get [\\"y\\"]() {} + + a = 1 + static [\\"y\\"]() {} + + a = 1 + set [\\"z\\"](z) {} + + a = 1 + async [\\"a\\"]() {} + + a = 1 + async *g() {} + + a = 0 + b = 1 +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x + ;(() => {})() +} + +;// flow + +(x: void) +;(y: void) + +// check statement clauses + +do + break +while (false) +if (true) + do + break + while (false) + +if (true) 1 +else 2 +for (;;); +for (x of y); + +debugger + +// check that it doesn't break non-ASI + +1 - 1 + +1 + 1 + +1 / 1 + +arr[0] + +fn(x) + +!1 + +1 < 1 + +tag\`string\` + +x +x => x + +while (false) + (function() {})() + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c) +" +`; + +exports[`no-semi.js 3`] = ` +" +// with preexisting semi + +x; [1, 2, 3].forEach(fn) +x; [a, b, ...c] = [1, 2] +x; /r/i.test('r') +x; +1 +x; - 1 +x; ('h' + 'i').repeat(10) +x; (1, 2) +x; (() => {})() +x; ({ a: 1 }).entries() +x; ({ a: 1 }).entries() +x; +x; \`string\` +x; (x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} [1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b](){} + + c = 0; + *d(){} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + + q() {}; + [h](){} + + p() {}; + *i(){} + + a = 1; + get ['y']() {} + + a = 1; + static ['y']() {} + + a = 1; + set ['z'](z) {} + + a = 1; + async ['a']() {} + + a = 1; + async *g() {} + + a = 0; + b = 1; +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x; (() => {})() +} + +// flow + +(x: void); +(y: void) + +// check statement clauses + +do break; while (false) +if (true) do break; while (false) + +if (true) 1; else 2 +for (;;) ; +for (x of y) ; + +debugger + +// check that it doesn't break non-ASI + +1 +- 1 + +1 ++ 1 + +1 +/ 1 + +arr +[0] + +fn +(x) + +!1 + +1 +< 1 + +tag +\`string\` + +x; x => x + +while (false) + (function(){}()) + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// with preexisting semi + +x +;[1, 2, 3].forEach(fn) +x +;[a, b, ...c] = [1, 2] +x +;/r/i.test(\\"r\\") +x +;+1 +x +;-1 +x +;(\\"h\\" + \\"i\\").repeat(10) +x +1, 2 +x +;(() => {})() +x +;({ a: 1 }.entries()) +x +;({ a: 1 }.entries()) +x +; +x +;\`string\` +x +;(x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} +;[1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b]() {} + + c = 0; + *d() {} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + + q() {} + [h]() {} + + p() {} + *i() {} + + a = 1 + get [\\"y\\"]() {} + + a = 1 + static [\\"y\\"]() {} + + a = 1 + set [\\"z\\"](z) {} + + a = 1 + async [\\"a\\"]() {} + + a = 1 + async *g() {} + + a = 0 + b = 1 +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x + ;(() => {})() +} + +;// flow + +(x: void) +;(y: void) + +// check statement clauses + +do + break +while (false) +if (true) + do + break + while (false) + +if (true) 1 +else 2 +for (;;); +for (x of y); + +debugger + +// check that it doesn't break non-ASI + +1 - 1 + +1 + 1 + +1 / 1 + +arr[0] + +fn(x) + +!1 + +1 < 1 + +tag\`string\` + +x +x => x + +while (false) + (function() {})() + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c) +" +`; diff --git a/tests/no-semi/jsfmt.spec.js b/tests/no-semi/jsfmt.spec.js new file mode 100644 index 00000000..911df21c --- /dev/null +++ b/tests/no-semi/jsfmt.spec.js @@ -0,0 +1,3 @@ +run_spec(__dirname); +run_spec(__dirname, { semi: false, parser: "flow" }); +run_spec(__dirname, { semi: false, parser: "babylon" }); diff --git a/tests/no-semi/no-semi.js b/tests/no-semi/no-semi.js new file mode 100644 index 00000000..8f729135 --- /dev/null +++ b/tests/no-semi/no-semi.js @@ -0,0 +1,152 @@ + +// with preexisting semi + +x; [1, 2, 3].forEach(fn) +x; [a, b, ...c] = [1, 2] +x; /r/i.test('r') +x; +1 +x; - 1 +x; ('h' + 'i').repeat(10) +x; (1, 2) +x; (() => {})() +x; ({ a: 1 }).entries() +x; ({ a: 1 }).entries() +x; +x; `string` +x; (x, y) => x + +// doesn't have to be preceded by a semicolon + +class X {} [1, 2, 3].forEach(fn) + +// TODO: enable lang features +// x; ::b.c +// TODO: upgrade parser +// class A { +// async; // The semicolon is *not* necessary +// x(){} +// } +// class B { +// static; // The semicolon *is* necessary +// x(){} +// } +// class C { +// get; // The semicolon *is* necessary +// x(){} +// } +// class C { +// set; // The semicolon *is* necessary +// x(){} +// } + + +// don't semicolon if it doesn't start statement + +if (true) (() => {})() + +class A { + a = 0; + [b](){} + + c = 0; + *d(){} + + e = 0; + [f] = 0 + + // none of the semicolons above this comment can be omitted. + // none of the semicolons below this comment are necessary. + + q() {}; + [h](){} + + p() {}; + *i(){} + + a = 1; + get ['y']() {} + + a = 1; + static ['y']() {} + + a = 1; + set ['z'](z) {} + + a = 1; + async ['a']() {} + + a = 1; + async *g() {} + + a = 0; + b = 1; +} + +// being first/last shouldn't break things +class G { + x = 1 +} +class G { + x() {} +} +class G { + *x() {} +} +class G { + [x] = 1 +} + +// check indentation + +if (true) { + x; (() => {})() +} + +// flow + +(x: void); +(y: void) + +// check statement clauses + +do break; while (false) +if (true) do break; while (false) + +if (true) 1; else 2 +for (;;) ; +for (x of y) ; + +debugger + +// check that it doesn't break non-ASI + +1 +- 1 + +1 ++ 1 + +1 +/ 1 + +arr +[0] + +fn +(x) + +!1 + +1 +< 1 + +tag +`string` + +x; x => x + +while (false) + (function(){}()) + +aReallyLongLine012345678901234567890123456789012345678901234567890123456789 * + (b + c)