Skip to content

Commit

Permalink
Update: add beforeStatementContinuationChars to semi (fixes #9521) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea authored and not-an-aardvark committed Nov 26, 2017
1 parent 4118f14 commit 71eedbf
Show file tree
Hide file tree
Showing 3 changed files with 609 additions and 39 deletions.
42 changes: 41 additions & 1 deletion docs/rules/semi.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,16 @@ String option:
* `"always"` (default) requires semicolons at the end of statements
* `"never"` disallows semicolons as the end of statements (except to disambiguate statements beginning with `[`, `(`, `/`, `+`, or `-`)

Object option:
Object option (when `"always"`):

* `"omitLastInOneLineBlock": true` ignores the last semicolon in a block in which its braces (and therefore the content of the block) are in the same line

Object option (when `"never"`):

* `"beforeStatementContinuationChars": "any"` (default) ignores semicolons (or lacking semicolon) at the end of statements if the next line starts with `[`, `(`, `/`, `+`, or `-`.
* `"beforeStatementContinuationChars": "always"` requires semicolons at the end of statements if the next line starts with `[`, `(`, `/`, `+`, or `-`.
* `"beforeStatementContinuationChars": "never"` disallows semicolons as the end of statements if it doesn't make ASI hazard even if the next line starts with `[`, `(`, `/`, `+`, or `-`.

### always

Examples of **incorrect** code for this rule with the default `"always"` option:
Expand Down Expand Up @@ -123,6 +129,16 @@ object.method = function() {

var name = "ESLint"

;(function() {
// ...
})()

import a from "a"
(function() {
// ...
})()

import b from "b"
;(function() {
// ...
})()
Expand All @@ -140,6 +156,30 @@ if (foo) { bar() }
if (foo) { bar(); baz() }
```

#### beforeStatementContinuationChars

Examples of additional **incorrect** code for this rule with the `"never", { "beforeStatementContinuationChars": "always" }` options:

```js
/*eslint semi: ["error", "never", { "beforeStatementContinuationChars": "always"}] */
import a from "a"

(function() {
// ...
})()
```

Examples of additional **incorrect** code for this rule with the `"never", { "beforeStatementContinuationChars": "never" }` options:

```js
/*eslint semi: ["error", "never", { "beforeStatementContinuationChars": "never"}] */
import a from "a"

;(function() {
// ...
})()
```

## When Not To Use It

If you do not want to enforce semicolon usage (or omission) in any particular way, then you can turn this rule off.
Expand Down
169 changes: 132 additions & 37 deletions lib/rules/semi.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,19 @@ module.exports = {
items: [
{
enum: ["never"]
},
{
type: "object",
properties: {
beforeStatementContinuationChars: {
enum: ["always", "any", "never"]
}
},
additionalProperties: false
}
],
minItems: 0,
maxItems: 1
maxItems: 2
},
{
type: "array",
Expand All @@ -62,9 +71,10 @@ module.exports = {

const OPT_OUT_PATTERN = /^[-[(/+`]/; // One of [(/+-`
const options = context.options[1];
const never = context.options[0] === "never",
exceptOneLine = options && options.omitLastInOneLineBlock === true,
sourceCode = context.getSourceCode();
const never = context.options[0] === "never";
const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock);
const beforeStatementContinuationChars = (options && options.beforeStatementContinuationChars) || "any";
const sourceCode = context.getSourceCode();

//--------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -114,29 +124,115 @@ module.exports = {
}

/**
* Check if a semicolon is unnecessary, only true if:
* - next token is on a new line and is not one of the opt-out tokens
* - next token is a valid statement divider
* @param {Token} lastToken last token of current node.
* @returns {boolean} whether the semicolon is unnecessary.
* Check whether a given semicolon token is redandant.
* @param {Token} semiToken A semicolon token to check.
* @returns {boolean} `true` if the next token is `;` or `}`.
*/
function isRedundantSemi(semiToken) {
const nextToken = sourceCode.getTokenAfter(semiToken);

return (
!nextToken ||
astUtils.isClosingBraceToken(nextToken) ||
astUtils.isSemicolonToken(nextToken)
);
}

/**
* Check whether a given token is the closing brace of an arrow function.
* @param {Token} lastToken A token to check.
* @returns {boolean} `true` if the token is the closing brace of an arrow function.
*/
function isUnnecessarySemicolon(lastToken) {
if (!astUtils.isSemicolonToken(lastToken)) {
function isEndOfArrowBlock(lastToken) {
if (!astUtils.isClosingBraceToken(lastToken)) {
return false;
}
const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]);

const nextToken = sourceCode.getTokenAfter(lastToken);
return (
node.type === "BlockStatement" &&
node.parent.type === "ArrowFunctionExpression"
);
}

/**
* Check whether a given node is on the same line with the next token.
* @param {Node} node A statement node to check.
* @returns {boolean} `true` if the node is on the same line with the next token.
*/
function isOnSameLineWithNextToken(node) {
const prevToken = sourceCode.getLastToken(node, 1);
const nextToken = sourceCode.getTokenAfter(node);

return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken);
}

/**
* Check whether a given node can connect the next line if the next line is unreliable.
* @param {Node} node A statement node to check.
* @returns {boolean} `true` if the node can connect the next line.
*/
function maybeAsiHazardAfter(node) {
const t = node.type;

if (!nextToken) {
return true;
if (t === "DoWhileStatement" ||
t === "BreakStatement" ||
t === "ContinueStatement" ||
t === "DebuggerStatement" ||
t === "ImportDeclaration" ||
t === "ExportAllDeclaration"
) {
return false;
}
if (t === "ReturnStatement") {
return Boolean(node.argument);
}
if (t === "ExportNamedDeclaration") {
return Boolean(node.declaration);
}
if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) {
return false;
}

const lastTokenLine = lastToken.loc.end.line;
const nextTokenLine = nextToken.loc.start.line;
const isOptOutToken = OPT_OUT_PATTERN.test(nextToken.value) && nextToken.value !== "++" && nextToken.value !== "--";
const isDivider = (astUtils.isClosingBraceToken(nextToken) || astUtils.isSemicolonToken(nextToken));
return true;
}

return (lastTokenLine !== nextTokenLine && !isOptOutToken) || isDivider;
/**
* Check whether a given token can connect the previous statement.
* @param {Token} token A token to check.
* @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`.
*/
function maybeAsiHazardBefore(token) {
return (
Boolean(token) &&
OPT_OUT_PATTERN.test(token.value) &&
token.value !== "++" &&
token.value !== "--"
);
}

/**
* Check if the semicolon of a given node is unnecessary, only true if:
* - next token is a valid statement divider (`;` or `}`).
* - next token is on a new line and the node is not connectable to the new line.
* @param {Node} node A statement node to check.
* @returns {boolean} whether the semicolon is unnecessary.
*/
function canRemoveSemicolon(node) {
if (isRedundantSemi(sourceCode.getLastToken(node))) {
return true; // `;;` or `;}`
}
if (isOnSameLineWithNextToken(node)) {
return false; // One liner.
}
if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) {
return true; // ASI works. This statement doesn't connect to the next.
}
if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
return true; // ASI works. The next token doesn't connect to this statement.
}

return false;
}

/**
Expand All @@ -145,16 +241,17 @@ module.exports = {
* @returns {boolean} whether the node is in a one-liner block statement.
*/
function isOneLinerBlock(node) {
const parent = node.parent;
const nextToken = sourceCode.getTokenAfter(node);

if (!nextToken || nextToken.value !== "}") {
return false;
}

const parent = node.parent;

return parent && parent.type === "BlockStatement" &&
parent.loc.start.line === parent.loc.end.line;
return (
!!parent &&
parent.type === "BlockStatement" &&
parent.loc.start.line === parent.loc.end.line
);
}

/**
Expand All @@ -163,21 +260,21 @@ module.exports = {
* @returns {void}
*/
function checkForSemicolon(node) {
const lastToken = sourceCode.getLastToken(node);
const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node));

if (never) {
if (isUnnecessarySemicolon(lastToken)) {
if (isSemi && canRemoveSemicolon(node)) {
report(node, true);
} else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
report(node);
}
} else {
if (!astUtils.isSemicolonToken(lastToken)) {
if (!exceptOneLine || !isOneLinerBlock(node)) {
report(node);
}
} else {
if (exceptOneLine && isOneLinerBlock(node)) {
report(node, true);
}
const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node));

if (isSemi && oneLinerBlock) {
report(node, true);
} else if (!isSemi && !oneLinerBlock) {
report(node);
}
}
}
Expand All @@ -188,9 +285,7 @@ module.exports = {
* @returns {void}
*/
function checkForSemicolonForVariableDeclaration(node) {
const ancestors = context.getAncestors(),
parentIndex = ancestors.length - 1,
parent = ancestors[parentIndex];
const parent = node.parent;

if ((parent.type !== "ForStatement" || parent.init !== node) &&
(!/^For(?:In|Of)Statement/.test(parent.type) || parent.left !== node)
Expand Down

0 comments on commit 71eedbf

Please sign in to comment.