| /** |
| * @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)$/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 |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "layout", |
| |
| docs: { |
| description: "enforce consistent spacing before and after keywords", |
| category: "Stylistic Issues", |
| 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", default: true }, |
| after: { type: "boolean", default: true } |
| }, |
| 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(); |
| |
| /** |
| * 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) && |
| astUtils.isTokenOnSameLine(prevToken, token) && |
| !sourceCode.isSpaceBetweenTokens(prevToken, token) |
| ) { |
| context.report({ |
| loc: token.loc.start, |
| 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) && |
| astUtils.isTokenOnSameLine(prevToken, token) && |
| sourceCode.isSpaceBetweenTokens(prevToken, token) |
| ) { |
| context.report({ |
| loc: 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) && |
| astUtils.isTokenOnSameLine(token, nextToken) && |
| !sourceCode.isSpaceBetweenTokens(token, nextToken) |
| ) { |
| context.report({ |
| loc: token.loc.start, |
| 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) && |
| astUtils.isTokenOnSameLine(token, nextToken) && |
| sourceCode.isSpaceBetweenTokens(token, nextToken) |
| ) { |
| context.report({ |
| loc: token.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|undefined} 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|undefined} 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); |
| checkSpacingAroundTokenBefore(node.right); |
| } |
| |
| /** |
| * 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); |
| } |
| checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken)); |
| } |
| |
| /** |
| * 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.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 - 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. |
| * @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 |
| ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier, |
| MethodDefinition: checkSpacingForProperty, |
| Property: checkSpacingForProperty |
| }; |
| } |
| }; |