573 lines
21 KiB
JavaScript
573 lines
21 KiB
JavaScript
/**
|
|
* @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" },
|
|
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();
|
|
|
|
/**
|
|
* 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,
|
|
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: { 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) &&
|
|
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) &&
|
|
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);
|
|
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.type === "ExportAllDeclaration" && node.exported) {
|
|
const asToken = sourceCode.getTokenBefore(node.exported);
|
|
|
|
checkSpacingBefore(asToken, PREV_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 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
|
|
};
|
|
}
|
|
};
|