You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
637 lines
24 KiB
637 lines
24 KiB
/** |
|
* @fileoverview Rule to enforce spacing before and after keywords. |
|
* @author Toru Nagashima |
|
*/ |
|
|
|
"use strict"; |
|
|
|
//------------------------------------------------------------------------------ |
|
// Requirements |
|
//------------------------------------------------------------------------------ |
|
|
|
const astUtils = require("./utils/ast-utils"), |
|
keywords = require("./utils/keywords"); |
|
|
|
//------------------------------------------------------------------------------ |
|
// Constants |
|
//------------------------------------------------------------------------------ |
|
|
|
const PREV_TOKEN = /^[)\]}>]$/u; |
|
const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/u; |
|
const PREV_TOKEN_M = /^[)\]}>*]$/u; |
|
const NEXT_TOKEN_M = /^[{*]$/u; |
|
const TEMPLATE_OPEN_PAREN = /\$\{$/u; |
|
const TEMPLATE_CLOSE_PAREN = /^\}/u; |
|
const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template|PrivateIdentifier)$/u; |
|
const KEYS = keywords.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]); |
|
|
|
// check duplications. |
|
(function() { |
|
KEYS.sort(); |
|
for (let i = 1; i < KEYS.length; ++i) { |
|
if (KEYS[i] === KEYS[i - 1]) { |
|
throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`); |
|
} |
|
} |
|
}()); |
|
|
|
//------------------------------------------------------------------------------ |
|
// Helpers |
|
//------------------------------------------------------------------------------ |
|
|
|
/** |
|
* Checks whether or not a given token is a "Template" token ends with "${". |
|
* @param {Token} token A token to check. |
|
* @returns {boolean} `true` if the token is a "Template" token ends with "${". |
|
*/ |
|
function isOpenParenOfTemplate(token) { |
|
return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value); |
|
} |
|
|
|
/** |
|
* Checks whether or not a given token is a "Template" token starts with "}". |
|
* @param {Token} token A token to check. |
|
* @returns {boolean} `true` if the token is a "Template" token starts with "}". |
|
*/ |
|
function isCloseParenOfTemplate(token) { |
|
return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Rule Definition |
|
//------------------------------------------------------------------------------ |
|
|
|
/** @type {import('../shared/types').Rule} */ |
|
module.exports = { |
|
meta: { |
|
type: "layout", |
|
|
|
docs: { |
|
description: "Enforce consistent spacing before and after keywords", |
|
recommended: false, |
|
url: "https://eslint.org/docs/rules/keyword-spacing" |
|
}, |
|
|
|
fixable: "whitespace", |
|
|
|
schema: [ |
|
{ |
|
type: "object", |
|
properties: { |
|
before: { type: "boolean", default: true }, |
|
after: { type: "boolean", default: true }, |
|
overrides: { |
|
type: "object", |
|
properties: KEYS.reduce((retv, key) => { |
|
retv[key] = { |
|
type: "object", |
|
properties: { |
|
before: { type: "boolean" }, |
|
after: { type: "boolean" } |
|
}, |
|
additionalProperties: false |
|
}; |
|
return retv; |
|
}, {}), |
|
additionalProperties: false |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
messages: { |
|
expectedBefore: "Expected space(s) before \"{{value}}\".", |
|
expectedAfter: "Expected space(s) after \"{{value}}\".", |
|
unexpectedBefore: "Unexpected space(s) before \"{{value}}\".", |
|
unexpectedAfter: "Unexpected space(s) after \"{{value}}\"." |
|
} |
|
}, |
|
|
|
create(context) { |
|
const sourceCode = context.getSourceCode(); |
|
|
|
const tokensToIgnore = new WeakSet(); |
|
|
|
/** |
|
* Reports a given token if there are not space(s) before the token. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} pattern A pattern of the previous token to check. |
|
* @returns {void} |
|
*/ |
|
function expectSpaceBefore(token, pattern) { |
|
const prevToken = sourceCode.getTokenBefore(token); |
|
|
|
if (prevToken && |
|
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) && |
|
!isOpenParenOfTemplate(prevToken) && |
|
!tokensToIgnore.has(prevToken) && |
|
astUtils.isTokenOnSameLine(prevToken, token) && |
|
!sourceCode.isSpaceBetweenTokens(prevToken, token) |
|
) { |
|
context.report({ |
|
loc: token.loc, |
|
messageId: "expectedBefore", |
|
data: token, |
|
fix(fixer) { |
|
return fixer.insertTextBefore(token, " "); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
/** |
|
* Reports a given token if there are space(s) before the token. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} pattern A pattern of the previous token to check. |
|
* @returns {void} |
|
*/ |
|
function unexpectSpaceBefore(token, pattern) { |
|
const prevToken = sourceCode.getTokenBefore(token); |
|
|
|
if (prevToken && |
|
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) && |
|
!isOpenParenOfTemplate(prevToken) && |
|
!tokensToIgnore.has(prevToken) && |
|
astUtils.isTokenOnSameLine(prevToken, token) && |
|
sourceCode.isSpaceBetweenTokens(prevToken, token) |
|
) { |
|
context.report({ |
|
loc: { start: prevToken.loc.end, end: token.loc.start }, |
|
messageId: "unexpectedBefore", |
|
data: token, |
|
fix(fixer) { |
|
return fixer.removeRange([prevToken.range[1], token.range[0]]); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
/** |
|
* Reports a given token if there are not space(s) after the token. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} pattern A pattern of the next token to check. |
|
* @returns {void} |
|
*/ |
|
function expectSpaceAfter(token, pattern) { |
|
const nextToken = sourceCode.getTokenAfter(token); |
|
|
|
if (nextToken && |
|
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) && |
|
!isCloseParenOfTemplate(nextToken) && |
|
!tokensToIgnore.has(nextToken) && |
|
astUtils.isTokenOnSameLine(token, nextToken) && |
|
!sourceCode.isSpaceBetweenTokens(token, nextToken) |
|
) { |
|
context.report({ |
|
loc: token.loc, |
|
messageId: "expectedAfter", |
|
data: token, |
|
fix(fixer) { |
|
return fixer.insertTextAfter(token, " "); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
/** |
|
* Reports a given token if there are space(s) after the token. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} pattern A pattern of the next token to check. |
|
* @returns {void} |
|
*/ |
|
function unexpectSpaceAfter(token, pattern) { |
|
const nextToken = sourceCode.getTokenAfter(token); |
|
|
|
if (nextToken && |
|
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) && |
|
!isCloseParenOfTemplate(nextToken) && |
|
!tokensToIgnore.has(nextToken) && |
|
astUtils.isTokenOnSameLine(token, nextToken) && |
|
sourceCode.isSpaceBetweenTokens(token, nextToken) |
|
) { |
|
|
|
context.report({ |
|
loc: { start: token.loc.end, end: nextToken.loc.start }, |
|
messageId: "unexpectedAfter", |
|
data: token, |
|
fix(fixer) { |
|
return fixer.removeRange([token.range[1], nextToken.range[0]]); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
/** |
|
* Parses the option object and determines check methods for each keyword. |
|
* @param {Object|undefined} options The option object to parse. |
|
* @returns {Object} - Normalized option object. |
|
* Keys are keywords (there are for every keyword). |
|
* Values are instances of `{"before": function, "after": function}`. |
|
*/ |
|
function parseOptions(options = {}) { |
|
const before = options.before !== false; |
|
const after = options.after !== false; |
|
const defaultValue = { |
|
before: before ? expectSpaceBefore : unexpectSpaceBefore, |
|
after: after ? expectSpaceAfter : unexpectSpaceAfter |
|
}; |
|
const overrides = (options && options.overrides) || {}; |
|
const retv = Object.create(null); |
|
|
|
for (let i = 0; i < KEYS.length; ++i) { |
|
const key = KEYS[i]; |
|
const override = overrides[key]; |
|
|
|
if (override) { |
|
const thisBefore = ("before" in override) ? override.before : before; |
|
const thisAfter = ("after" in override) ? override.after : after; |
|
|
|
retv[key] = { |
|
before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore, |
|
after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter |
|
}; |
|
} else { |
|
retv[key] = defaultValue; |
|
} |
|
} |
|
|
|
return retv; |
|
} |
|
|
|
const checkMethodMap = parseOptions(context.options[0]); |
|
|
|
/** |
|
* Reports a given token if usage of spacing followed by the token is |
|
* invalid. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} [pattern] Optional. A pattern of the previous |
|
* token to check. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingBefore(token, pattern) { |
|
checkMethodMap[token.value].before(token, pattern || PREV_TOKEN); |
|
} |
|
|
|
/** |
|
* Reports a given token if usage of spacing preceded by the token is |
|
* invalid. |
|
* @param {Token} token A token to report. |
|
* @param {RegExp} [pattern] Optional. A pattern of the next |
|
* token to check. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingAfter(token, pattern) { |
|
checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN); |
|
} |
|
|
|
/** |
|
* Reports a given token if usage of spacing around the token is invalid. |
|
* @param {Token} token A token to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingAround(token) { |
|
checkSpacingBefore(token); |
|
checkSpacingAfter(token); |
|
} |
|
|
|
/** |
|
* Reports the first token of a given node if the first token is a keyword |
|
* and usage of spacing around the token is invalid. |
|
* @param {ASTNode|null} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingAroundFirstToken(node) { |
|
const firstToken = node && sourceCode.getFirstToken(node); |
|
|
|
if (firstToken && firstToken.type === "Keyword") { |
|
checkSpacingAround(firstToken); |
|
} |
|
} |
|
|
|
/** |
|
* Reports the first token of a given node if the first token is a keyword |
|
* and usage of spacing followed by the token is invalid. |
|
* |
|
* This is used for unary operators (e.g. `typeof`), `function`, and `super`. |
|
* Other rules are handling usage of spacing preceded by those keywords. |
|
* @param {ASTNode|null} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingBeforeFirstToken(node) { |
|
const firstToken = node && sourceCode.getFirstToken(node); |
|
|
|
if (firstToken && firstToken.type === "Keyword") { |
|
checkSpacingBefore(firstToken); |
|
} |
|
} |
|
|
|
/** |
|
* Reports the previous token of a given node if the token is a keyword and |
|
* usage of spacing around the token is invalid. |
|
* @param {ASTNode|null} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingAroundTokenBefore(node) { |
|
if (node) { |
|
const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken); |
|
|
|
checkSpacingAround(token); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `async` or `function` keywords of a given node if usage of |
|
* spacing around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForFunction(node) { |
|
const firstToken = node && sourceCode.getFirstToken(node); |
|
|
|
if (firstToken && |
|
((firstToken.type === "Keyword" && firstToken.value === "function") || |
|
firstToken.value === "async") |
|
) { |
|
checkSpacingBefore(firstToken); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `class` and `extends` keywords of a given node if usage of |
|
* spacing around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForClass(node) { |
|
checkSpacingAroundFirstToken(node); |
|
checkSpacingAroundTokenBefore(node.superClass); |
|
} |
|
|
|
/** |
|
* Reports `if` and `else` keywords of a given node if usage of spacing |
|
* around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForIfStatement(node) { |
|
checkSpacingAroundFirstToken(node); |
|
checkSpacingAroundTokenBefore(node.alternate); |
|
} |
|
|
|
/** |
|
* Reports `try`, `catch`, and `finally` keywords of a given node if usage |
|
* of spacing around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForTryStatement(node) { |
|
checkSpacingAroundFirstToken(node); |
|
checkSpacingAroundFirstToken(node.handler); |
|
checkSpacingAroundTokenBefore(node.finalizer); |
|
} |
|
|
|
/** |
|
* Reports `do` and `while` keywords of a given node if usage of spacing |
|
* around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForDoWhileStatement(node) { |
|
checkSpacingAroundFirstToken(node); |
|
checkSpacingAroundTokenBefore(node.test); |
|
} |
|
|
|
/** |
|
* Reports `for` and `in` keywords of a given node if usage of spacing |
|
* around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForForInStatement(node) { |
|
checkSpacingAroundFirstToken(node); |
|
|
|
const inToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken); |
|
const previousToken = sourceCode.getTokenBefore(inToken); |
|
|
|
if (previousToken.type !== "PrivateIdentifier") { |
|
checkSpacingBefore(inToken); |
|
} |
|
|
|
checkSpacingAfter(inToken); |
|
} |
|
|
|
/** |
|
* Reports `for` and `of` keywords of a given node if usage of spacing |
|
* around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForForOfStatement(node) { |
|
if (node.await) { |
|
checkSpacingBefore(sourceCode.getFirstToken(node, 0)); |
|
checkSpacingAfter(sourceCode.getFirstToken(node, 1)); |
|
} else { |
|
checkSpacingAroundFirstToken(node); |
|
} |
|
|
|
const ofToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken); |
|
const previousToken = sourceCode.getTokenBefore(ofToken); |
|
|
|
if (previousToken.type !== "PrivateIdentifier") { |
|
checkSpacingBefore(ofToken); |
|
} |
|
|
|
checkSpacingAfter(ofToken); |
|
} |
|
|
|
/** |
|
* Reports `import`, `export`, `as`, and `from` keywords of a given node if |
|
* usage of spacing around those keywords is invalid. |
|
* |
|
* This rule handles the `*` token in module declarations. |
|
* |
|
* import*as A from "./a"; /*error Expected space(s) after "import". |
|
* error Expected space(s) before "as". |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForModuleDeclaration(node) { |
|
const firstToken = sourceCode.getFirstToken(node); |
|
|
|
checkSpacingBefore(firstToken, PREV_TOKEN_M); |
|
checkSpacingAfter(firstToken, NEXT_TOKEN_M); |
|
|
|
if (node.type === "ExportDefaultDeclaration") { |
|
checkSpacingAround(sourceCode.getTokenAfter(firstToken)); |
|
} |
|
|
|
if (node.type === "ExportAllDeclaration" && node.exported) { |
|
const asToken = sourceCode.getTokenBefore(node.exported); |
|
|
|
checkSpacingBefore(asToken, PREV_TOKEN_M); |
|
checkSpacingAfter(asToken, NEXT_TOKEN_M); |
|
} |
|
|
|
if (node.source) { |
|
const fromToken = sourceCode.getTokenBefore(node.source); |
|
|
|
checkSpacingBefore(fromToken, PREV_TOKEN_M); |
|
checkSpacingAfter(fromToken, NEXT_TOKEN_M); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `as` keyword of a given node if usage of spacing around this |
|
* keyword is invalid. |
|
* @param {ASTNode} node An `ImportSpecifier` node to check. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForImportSpecifier(node) { |
|
if (node.imported.range[0] !== node.local.range[0]) { |
|
const asToken = sourceCode.getTokenBefore(node.local); |
|
|
|
checkSpacingBefore(asToken, PREV_TOKEN_M); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `as` keyword of a given node if usage of spacing around this |
|
* keyword is invalid. |
|
* @param {ASTNode} node An `ExportSpecifier` node to check. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForExportSpecifier(node) { |
|
if (node.local.range[0] !== node.exported.range[0]) { |
|
const asToken = sourceCode.getTokenBefore(node.exported); |
|
|
|
checkSpacingBefore(asToken, PREV_TOKEN_M); |
|
checkSpacingAfter(asToken, NEXT_TOKEN_M); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `as` keyword of a given node if usage of spacing around this |
|
* keyword is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForImportNamespaceSpecifier(node) { |
|
const asToken = sourceCode.getFirstToken(node, 1); |
|
|
|
checkSpacingBefore(asToken, PREV_TOKEN_M); |
|
} |
|
|
|
/** |
|
* Reports `static`, `get`, and `set` keywords of a given node if usage of |
|
* spacing around those keywords is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @throws {Error} If unable to find token get, set, or async beside method name. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForProperty(node) { |
|
if (node.static) { |
|
checkSpacingAroundFirstToken(node); |
|
} |
|
if (node.kind === "get" || |
|
node.kind === "set" || |
|
( |
|
(node.method || node.type === "MethodDefinition") && |
|
node.value.async |
|
) |
|
) { |
|
const token = sourceCode.getTokenBefore( |
|
node.key, |
|
tok => { |
|
switch (tok.value) { |
|
case "get": |
|
case "set": |
|
case "async": |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
); |
|
|
|
if (!token) { |
|
throw new Error("Failed to find token get, set, or async beside method name"); |
|
} |
|
|
|
|
|
checkSpacingAround(token); |
|
} |
|
} |
|
|
|
/** |
|
* Reports `await` keyword of a given node if usage of spacing before |
|
* this keyword is invalid. |
|
* @param {ASTNode} node A node to report. |
|
* @returns {void} |
|
*/ |
|
function checkSpacingForAwaitExpression(node) { |
|
checkSpacingBefore(sourceCode.getFirstToken(node)); |
|
} |
|
|
|
return { |
|
|
|
// Statements |
|
DebuggerStatement: checkSpacingAroundFirstToken, |
|
WithStatement: checkSpacingAroundFirstToken, |
|
|
|
// Statements - Control flow |
|
BreakStatement: checkSpacingAroundFirstToken, |
|
ContinueStatement: checkSpacingAroundFirstToken, |
|
ReturnStatement: checkSpacingAroundFirstToken, |
|
ThrowStatement: checkSpacingAroundFirstToken, |
|
TryStatement: checkSpacingForTryStatement, |
|
|
|
// Statements - Choice |
|
IfStatement: checkSpacingForIfStatement, |
|
SwitchStatement: checkSpacingAroundFirstToken, |
|
SwitchCase: checkSpacingAroundFirstToken, |
|
|
|
// Statements - Loops |
|
DoWhileStatement: checkSpacingForDoWhileStatement, |
|
ForInStatement: checkSpacingForForInStatement, |
|
ForOfStatement: checkSpacingForForOfStatement, |
|
ForStatement: checkSpacingAroundFirstToken, |
|
WhileStatement: checkSpacingAroundFirstToken, |
|
|
|
// Statements - Declarations |
|
ClassDeclaration: checkSpacingForClass, |
|
ExportNamedDeclaration: checkSpacingForModuleDeclaration, |
|
ExportDefaultDeclaration: checkSpacingForModuleDeclaration, |
|
ExportAllDeclaration: checkSpacingForModuleDeclaration, |
|
FunctionDeclaration: checkSpacingForFunction, |
|
ImportDeclaration: checkSpacingForModuleDeclaration, |
|
VariableDeclaration: checkSpacingAroundFirstToken, |
|
|
|
// Expressions |
|
ArrowFunctionExpression: checkSpacingForFunction, |
|
AwaitExpression: checkSpacingForAwaitExpression, |
|
ClassExpression: checkSpacingForClass, |
|
FunctionExpression: checkSpacingForFunction, |
|
NewExpression: checkSpacingBeforeFirstToken, |
|
Super: checkSpacingBeforeFirstToken, |
|
ThisExpression: checkSpacingBeforeFirstToken, |
|
UnaryExpression: checkSpacingBeforeFirstToken, |
|
YieldExpression: checkSpacingBeforeFirstToken, |
|
|
|
// Others |
|
ImportSpecifier: checkSpacingForImportSpecifier, |
|
ExportSpecifier: checkSpacingForExportSpecifier, |
|
ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier, |
|
MethodDefinition: checkSpacingForProperty, |
|
PropertyDefinition: checkSpacingForProperty, |
|
StaticBlock: checkSpacingAroundFirstToken, |
|
Property: checkSpacingForProperty, |
|
|
|
// To avoid conflicts with `space-infix-ops`, e.g. `a > this.b` |
|
"BinaryExpression[operator='>']"(node) { |
|
const operatorToken = sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken); |
|
|
|
tokensToIgnore.add(operatorToken); |
|
} |
|
}; |
|
} |
|
};
|
|
|