Permalink
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/eslint/lib/rules/curly.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
486 lines (418 sloc)
18.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @fileoverview Rule to flag statements without curly braces | |
* @author Nicholas C. Zakas | |
*/ | |
"use strict"; | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
const astUtils = require("./utils/ast-utils"); | |
//------------------------------------------------------------------------------ | |
// Rule Definition | |
//------------------------------------------------------------------------------ | |
module.exports = { | |
meta: { | |
type: "suggestion", | |
docs: { | |
description: "enforce consistent brace style for all control statements", | |
category: "Best Practices", | |
recommended: false, | |
url: "https://eslint.org/docs/rules/curly" | |
}, | |
schema: { | |
anyOf: [ | |
{ | |
type: "array", | |
items: [ | |
{ | |
enum: ["all"] | |
} | |
], | |
minItems: 0, | |
maxItems: 1 | |
}, | |
{ | |
type: "array", | |
items: [ | |
{ | |
enum: ["multi", "multi-line", "multi-or-nest"] | |
}, | |
{ | |
enum: ["consistent"] | |
} | |
], | |
minItems: 0, | |
maxItems: 2 | |
} | |
] | |
}, | |
fixable: "code", | |
messages: { | |
missingCurlyAfter: "Expected { after '{{name}}'.", | |
missingCurlyAfterCondition: "Expected { after '{{name}}' condition.", | |
unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.", | |
unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition." | |
} | |
}, | |
create(context) { | |
const multiOnly = (context.options[0] === "multi"); | |
const multiLine = (context.options[0] === "multi-line"); | |
const multiOrNest = (context.options[0] === "multi-or-nest"); | |
const consistent = (context.options[1] === "consistent"); | |
const sourceCode = context.getSourceCode(); | |
//-------------------------------------------------------------------------- | |
// Helpers | |
//-------------------------------------------------------------------------- | |
/** | |
* Determines if a given node is a one-liner that's on the same line as it's preceding code. | |
* @param {ASTNode} node The node to check. | |
* @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code. | |
* @private | |
*/ | |
function isCollapsedOneLiner(node) { | |
const before = sourceCode.getTokenBefore(node); | |
const last = sourceCode.getLastToken(node); | |
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; | |
return before.loc.start.line === lastExcludingSemicolon.loc.end.line; | |
} | |
/** | |
* Determines if a given node is a one-liner. | |
* @param {ASTNode} node The node to check. | |
* @returns {boolean} True if the node is a one-liner. | |
* @private | |
*/ | |
function isOneLiner(node) { | |
if (node.type === "EmptyStatement") { | |
return true; | |
} | |
const first = sourceCode.getFirstToken(node); | |
const last = sourceCode.getLastToken(node); | |
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; | |
return first.loc.start.line === lastExcludingSemicolon.loc.end.line; | |
} | |
/** | |
* Determines if the given node is a lexical declaration (let, const, function, or class) | |
* @param {ASTNode} node The node to check | |
* @returns {boolean} True if the node is a lexical declaration | |
* @private | |
*/ | |
function isLexicalDeclaration(node) { | |
if (node.type === "VariableDeclaration") { | |
return node.kind === "const" || node.kind === "let"; | |
} | |
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration"; | |
} | |
/** | |
* Checks if the given token is an `else` token or not. | |
* @param {Token} token The token to check. | |
* @returns {boolean} `true` if the token is an `else` token. | |
*/ | |
function isElseKeywordToken(token) { | |
return token.value === "else" && token.type === "Keyword"; | |
} | |
/** | |
* Determines whether the given node has an `else` keyword token as the first token after. | |
* @param {ASTNode} node The node to check. | |
* @returns {boolean} `true` if the node is followed by an `else` keyword token. | |
*/ | |
function isFollowedByElseKeyword(node) { | |
const nextToken = sourceCode.getTokenAfter(node); | |
return Boolean(nextToken) && isElseKeywordToken(nextToken); | |
} | |
/** | |
* Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError. | |
* @param {Token} closingBracket The } token | |
* @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block. | |
*/ | |
function needsSemicolon(closingBracket) { | |
const tokenBefore = sourceCode.getTokenBefore(closingBracket); | |
const tokenAfter = sourceCode.getTokenAfter(closingBracket); | |
const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); | |
if (astUtils.isSemicolonToken(tokenBefore)) { | |
// If the last statement already has a semicolon, don't add another one. | |
return false; | |
} | |
if (!tokenAfter) { | |
// If there are no statements after this block, there is no need to add a semicolon. | |
return false; | |
} | |
if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") { | |
/* | |
* If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression), | |
* don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause | |
* a SyntaxError if it was followed by `else`. | |
*/ | |
return false; | |
} | |
if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) { | |
// If the next token is on the same line, insert a semicolon. | |
return true; | |
} | |
if (/^[([/`+-]/u.test(tokenAfter.value)) { | |
// If the next token starts with a character that would disrupt ASI, insert a semicolon. | |
return true; | |
} | |
if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) { | |
// If the last token is ++ or --, insert a semicolon to avoid disrupting ASI. | |
return true; | |
} | |
// Otherwise, do not insert a semicolon. | |
return false; | |
} | |
/** | |
* Determines whether the code represented by the given node contains an `if` statement | |
* that would become associated with an `else` keyword directly appended to that code. | |
* | |
* Examples where it returns `true`: | |
* | |
* if (a) | |
* foo(); | |
* | |
* if (a) { | |
* foo(); | |
* } | |
* | |
* if (a) | |
* foo(); | |
* else if (b) | |
* bar(); | |
* | |
* while (a) | |
* if (b) | |
* if(c) | |
* foo(); | |
* else | |
* bar(); | |
* | |
* Examples where it returns `false`: | |
* | |
* if (a) | |
* foo(); | |
* else | |
* bar(); | |
* | |
* while (a) { | |
* if (b) | |
* if(c) | |
* foo(); | |
* else | |
* bar(); | |
* } | |
* | |
* while (a) | |
* if (b) { | |
* if(c) | |
* foo(); | |
* } | |
* else | |
* bar(); | |
* @param {ASTNode} node Node representing the code to check. | |
* @returns {boolean} `true` if an `if` statement within the code would become associated with an `else` appended to that code. | |
*/ | |
function hasUnsafeIf(node) { | |
switch (node.type) { | |
case "IfStatement": | |
if (!node.alternate) { | |
return true; | |
} | |
return hasUnsafeIf(node.alternate); | |
case "ForStatement": | |
case "ForInStatement": | |
case "ForOfStatement": | |
case "LabeledStatement": | |
case "WithStatement": | |
case "WhileStatement": | |
return hasUnsafeIf(node.body); | |
default: | |
return false; | |
} | |
} | |
/** | |
* Determines whether the existing curly braces around the single statement are necessary to preserve the semantics of the code. | |
* The braces, which make the given block body, are necessary in either of the following situations: | |
* | |
* 1. The statement is a lexical declaration. | |
* 2. Without the braces, an `if` within the statement would become associated with an `else` after the closing brace: | |
* | |
* if (a) { | |
* if (b) | |
* foo(); | |
* } | |
* else | |
* bar(); | |
* | |
* if (a) | |
* while (b) | |
* while (c) { | |
* while (d) | |
* if (e) | |
* while(f) | |
* foo(); | |
* } | |
* else | |
* bar(); | |
* @param {ASTNode} node `BlockStatement` body with exactly one statement directly inside. The statement can have its own nested statements. | |
* @returns {boolean} `true` if the braces are necessary - removing them (replacing the given `BlockStatement` body with its single statement content) | |
* would change the semantics of the code or produce a syntax error. | |
*/ | |
function areBracesNecessary(node) { | |
const statement = node.body[0]; | |
return isLexicalDeclaration(statement) || | |
hasUnsafeIf(statement) && isFollowedByElseKeyword(node); | |
} | |
/** | |
* Prepares to check the body of a node to see if it's a block statement. | |
* @param {ASTNode} node The node to report if there's a problem. | |
* @param {ASTNode} body The body node to check for blocks. | |
* @param {string} name The name to report if there's a problem. | |
* @param {{ condition: boolean }} opts Options to pass to the report functions | |
* @returns {Object} a prepared check object, with "actual", "expected", "check" properties. | |
* "actual" will be `true` or `false` whether the body is already a block statement. | |
* "expected" will be `true` or `false` if the body should be a block statement or not, or | |
* `null` if it doesn't matter, depending on the rule options. It can be modified to change | |
* the final behavior of "check". | |
* "check" will be a function reporting appropriate problems depending on the other | |
* properties. | |
*/ | |
function prepareCheck(node, body, name, opts) { | |
const hasBlock = (body.type === "BlockStatement"); | |
let expected = null; | |
if (hasBlock && (body.body.length !== 1 || areBracesNecessary(body))) { | |
expected = true; | |
} else if (multiOnly) { | |
expected = false; | |
} else if (multiLine) { | |
if (!isCollapsedOneLiner(body)) { | |
expected = true; | |
} | |
// otherwise, the body is allowed to have braces or not to have braces | |
} else if (multiOrNest) { | |
if (hasBlock) { | |
const statement = body.body[0]; | |
const leadingCommentsInBlock = sourceCode.getCommentsBefore(statement); | |
expected = !isOneLiner(statement) || leadingCommentsInBlock.length > 0; | |
} else { | |
expected = !isOneLiner(body); | |
} | |
} else { | |
// default "all" | |
expected = true; | |
} | |
return { | |
actual: hasBlock, | |
expected, | |
check() { | |
if (this.expected !== null && this.expected !== this.actual) { | |
if (this.expected) { | |
context.report({ | |
node, | |
loc: body.loc, | |
messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter", | |
data: { | |
name | |
}, | |
fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`) | |
}); | |
} else { | |
context.report({ | |
node, | |
loc: body.loc, | |
messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter", | |
data: { | |
name | |
}, | |
fix(fixer) { | |
/* | |
* `do while` expressions sometimes need a space to be inserted after `do`. | |
* e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)` | |
*/ | |
const needsPrecedingSpace = node.type === "DoWhileStatement" && | |
sourceCode.getTokenBefore(body).range[1] === body.range[0] && | |
!astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 })); | |
const openingBracket = sourceCode.getFirstToken(body); | |
const closingBracket = sourceCode.getLastToken(body); | |
const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket); | |
if (needsSemicolon(closingBracket)) { | |
/* | |
* If removing braces would cause a SyntaxError due to multiple statements on the same line (or | |
* change the semantics of the code due to ASI), don't perform a fix. | |
*/ | |
return null; | |
} | |
const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) + | |
sourceCode.getText(lastTokenInBlock) + | |
sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]); | |
return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText); | |
} | |
}); | |
} | |
} | |
} | |
}; | |
} | |
/** | |
* Prepares to check the bodies of a "if", "else if" and "else" chain. | |
* @param {ASTNode} node The first IfStatement node of the chain. | |
* @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more | |
* information. | |
*/ | |
function prepareIfChecks(node) { | |
const preparedChecks = []; | |
for (let currentNode = node; currentNode; currentNode = currentNode.alternate) { | |
preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true })); | |
if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") { | |
preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else")); | |
break; | |
} | |
} | |
if (consistent) { | |
/* | |
* If any node should have or already have braces, make sure they | |
* all have braces. | |
* If all nodes shouldn't have braces, make sure they don't. | |
*/ | |
const expected = preparedChecks.some(preparedCheck => { | |
if (preparedCheck.expected !== null) { | |
return preparedCheck.expected; | |
} | |
return preparedCheck.actual; | |
}); | |
preparedChecks.forEach(preparedCheck => { | |
preparedCheck.expected = expected; | |
}); | |
} | |
return preparedChecks; | |
} | |
//-------------------------------------------------------------------------- | |
// Public | |
//-------------------------------------------------------------------------- | |
return { | |
IfStatement(node) { | |
const parent = node.parent; | |
const isElseIf = parent.type === "IfStatement" && parent.alternate === node; | |
if (!isElseIf) { | |
// This is a top `if`, check the whole `if-else-if` chain | |
prepareIfChecks(node).forEach(preparedCheck => { | |
preparedCheck.check(); | |
}); | |
} | |
// Skip `else if`, it's already checked (when the top `if` was visited) | |
}, | |
WhileStatement(node) { | |
prepareCheck(node, node.body, "while", { condition: true }).check(); | |
}, | |
DoWhileStatement(node) { | |
prepareCheck(node, node.body, "do").check(); | |
}, | |
ForStatement(node) { | |
prepareCheck(node, node.body, "for", { condition: true }).check(); | |
}, | |
ForInStatement(node) { | |
prepareCheck(node, node.body, "for-in").check(); | |
}, | |
ForOfStatement(node) { | |
prepareCheck(node, node.body, "for-of").check(); | |
} | |
}; | |
} | |
}; |