@yusufkandemir/eslint-plugin-lodash-template
Version:
ESLint plugin for John Resig-style micro template, Lodash's template, Underscore's template and EJS.
1,286 lines (1,181 loc) • 66.1 kB
JavaScript
"use strict";
const { getSourceCode } = require("eslint-compat-utils");
/**
* Normalize options.
* @param {number|string|undefined} type The type of indentation.
* @param {object} options Other options.
* @returns {object} Normalized options.
*/
function parseOptions(type, options) {
const ret = {
indentChar: " ",
indentSize: 2,
startIndent: 1,
switchCase: 0,
};
if (Number.isSafeInteger(type)) {
ret.indentSize = type;
} else if (type === "tab") {
ret.indentChar = "\t";
ret.indentSize = 1;
}
if (Number.isSafeInteger(options.switchCase)) {
ret.switchCase = options.switchCase;
}
if (Number.isSafeInteger(options.startIndent)) {
ret.startIndent = options.startIndent;
}
return ret;
}
/**
* Check whether the given token is an arrow.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is an arrow.
*/
function isArrow(token) {
return token && token.type === "Punctuator" && token.value === "=>";
}
/**
* Check whether the given token is a left parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left parenthesis.
*/
function isLeftParen(token) {
return token && token.type === "Punctuator" && token.value === "(";
}
/**
* Check whether the given token is a left parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `false` if the token is a left parenthesis.
*/
function isNotLeftParen(token) {
return token && (token.type !== "Punctuator" || token.value !== "(");
}
/**
* Check whether the given token is a right parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right parenthesis.
*/
function isRightParen(token) {
return token && token.type === "Punctuator" && token.value === ")";
}
/**
* Check whether the given token is a right parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `false` if the token is a right parenthesis.
*/
function isNotRightParen(token) {
return token && (token.type !== "Punctuator" || token.value !== ")");
}
/**
* Check whether the given token is a left brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left brace.
*/
function isLeftBrace(token) {
return token && token.type === "Punctuator" && token.value === "{";
}
/**
* Check whether the given token is a right brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right brace.
*/
function isRightBrace(token) {
return token && token.type === "Punctuator" && token.value === "}";
}
/**
* Check whether the given token is a left bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left bracket.
*/
function isLeftBracket(token) {
return token && token.type === "Punctuator" && token.value === "[";
}
/**
* Check whether the given token is a right bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right bracket.
*/
function isRightBracket(token) {
return token && token.type === "Punctuator" && token.value === "]";
}
/**
* Check whether the given token is a semicolon.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a semicolon.
*/
function isSemicolon(token) {
return token && token.type === "Punctuator" && token.value === ";";
}
/**
* Check whether the given token is a comma.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a comma.
*/
function isComma(token) {
return token && token.type === "Punctuator" && token.value === ",";
}
/**
* Get the last element.
* @param {Array} xs The array to get the last element.
* @returns {any|undefined} The last element or undefined.
*/
function last(xs) {
return xs.length === 0 ? undefined : xs[xs.length - 1];
}
module.exports = {
meta: {
docs: {
description:
"enforce consistent indentation to scriptlet in micro-template tag.",
category: "recommended",
url: "https://ota-meshi.github.io/eslint-plugin-lodash-template/rules/scriptlet-indent.html",
},
fixable: "whitespace",
messages: {
unexpectedIndentationCharacter:
"Expected {{expected}} character, but found {{actual}} character.",
unexpectedIndentation:
"Expected indentation of {{expectedIndent}} {{unit}}{{expectedIndentPlural}} but found {{actualIndent}} {{unit}}{{actualIndentPlural}}.",
unexpectedRelativeIndentation:
"Expected relative indentation of {{expectedIndent}} {{unit}}{{expectedIndentPlural}} but found {{actualIndent}} {{unit}}{{actualIndentPlural}}.",
unexpectedBaseIndentation:
"Expected base point indentation of {{expected}}, but found {{actual}}.",
missingBaseIndentation:
"Expected base point indentation of {{expected}}, but not found.",
},
schema: [
{
anyOf: [{ type: "integer", minimum: 1 }, { enum: ["tab"] }],
},
{
type: "object",
properties: {
startIndent: { type: "integer", minimum: 0 },
switchCase: { type: "integer", minimum: 0 },
},
additionalProperties: false,
},
],
type: "layout",
},
create(context) {
const sourceCode = getSourceCode(context);
if (!sourceCode.parserServices.getMicroTemplateService) {
return {};
}
const microTemplateService =
sourceCode.parserServices.getMicroTemplateService();
const options = parseOptions(
context.options[0],
context.options[1] || {},
);
const offsets = new Map();
const actualLineIndentTexts = new Map();
const unit = options.indentChar === "\t" ? "tab" : "space";
/**
* Get line text from the given number of line.
* @param {number} line The number of line.
* @returns {string} The line text .
*/
function getLineText(line) {
return sourceCode.getLines()[line - 1];
}
/**
* Get the actual indent text of the line which the given line is on.
* @param {number} line The line no.
* @returns {string} The text of actual indent.
*/
function getActualLineIndentText(line) {
let actualText = actualLineIndentTexts.get(line);
if (actualText === undefined) {
const lineText = getLineText(line);
const index = lineText.search(/\S/u);
if (index >= 0) {
actualText = lineText.slice(0, index);
} else {
actualText = lineText;
}
actualLineIndentTexts.set(line, actualText);
}
return actualText;
}
/**
* Set offset to the given location.
* @param {Location} loc The start index to set.
* @param {number} offset The offset of the tokens.
* @param {Token} baseToken The token of the base offset.
* @returns {void}
*/
function setOffsetToLoc(loc, offset, baseToken) {
const baseline = baseToken.loc.start.line;
if (baseline >= loc.line) {
return;
}
const actualText = getActualLineIndentText(loc.line);
if (actualText.length !== loc.column) {
return;
}
offsets.set(loc.line, {
actualText,
baseline,
offset,
expectedIndent: undefined,
});
}
/**
* Set offset to the given tokens.
* @param {Token|Token[]} token The token to set.
* @param {number} offset The offset of the tokens.
* @param {Token} baseToken The token of the base offset.
* @returns {void}
*/
function setOffsetToToken(token, offset, baseToken) {
if (Array.isArray(token)) {
for (const t of token) {
setOffsetToLoc(t.loc.start, offset, baseToken);
}
return;
}
setOffsetToLoc(token.loc.start, offset, baseToken);
}
/**
* Expected indent data
*/
class ExpectedIndent {
/**
* constructor
* @param {string} baseIndentText The base point indentation text.
* @param {number} indent The number of indent.
* @returns {void}
*/
constructor(baseIndentText, indent) {
this.baseIndentText = baseIndentText;
this.indent = indent;
}
/**
* Get the expected indentation text.
* @returns {string} The expected indentation text.
*/
get indentText() {
if (this._indentText === undefined) {
this._indentText =
this.baseIndentText +
options.indentChar.repeat(this.indent);
}
return this._indentText;
}
}
/**
* Calculate correct indentation of the top level in the given template tag.
* @param {Token} templateTag The template tag.
* @returns {ExpectedIndent} Correct indentation.
*/
function getTopLevelIndentByTemplateTag(templateTag) {
const baseIndentText = getActualLineIndentText(
templateTag.loc.start.line,
);
return new ExpectedIndent(
baseIndentText,
options.indentSize * options.startIndent,
);
}
/**
* Calculate correct indentation of the line of the given line.
* @param {number} line The number of line.
* @returns {ExpectedIndent} Correct indentation.
*/
function getExpectedIndent(line) {
const offset = offsets.get(line);
if (!offset) {
const index = sourceCode.getIndexFromLoc({ line, column: 0 });
const templateTag =
microTemplateService.getTemplateTagByRangeIndex(index);
return getTopLevelIndentByTemplateTag(templateTag);
}
if (offset.expectedIndent !== undefined) {
return offset.expectedIndent;
}
const baseLineTemplate = microTemplateService
.getMicroTemplateTokens()
.find((t) => t.loc.start.line === offset.baseline);
let baseIndent = undefined;
if (baseLineTemplate) {
baseIndent = getTopLevelIndentByTemplateTag(baseLineTemplate);
} else {
baseIndent = getExpectedIndent(offset.baseline);
}
const expectedNumberOfIndent =
baseIndent.indent + offset.offset * options.indentSize;
const expectedIndent = new ExpectedIndent(
baseIndent.baseIndentText,
expectedNumberOfIndent,
);
offset.expectedIndent = expectedIndent;
return expectedIndent;
}
/**
* Find the head of chaining nodes.
* @param {Node} node The start node to find the head.
* @returns {Token} The head token of the chain.
*/
function getChainHeadToken(node) {
let target = node;
const type = target.type;
while (target.parent.type === type) {
target = target.parent;
}
return sourceCode.getFirstToken(target);
}
/**
* Get the first and last tokens of the given node.
* If the node is parenthesized, this gets the outermost parentheses.
* @param {Node} node The node to get.
* @param {number} [borderOffset] The least offset of the first token. Default is 0. This value is used to prevent false positive in the following case: `(a) => {}` The parentheses are enclosing the whole parameter part rather than the first parameter, but this offset parameter is needed to distinguish.
* @returns {{firstToken:Token,lastToken:Token}} The gotten tokens.
*/
function getFirstAndLastTokens(node, borderOffset) {
borderOffset |= 0; // eslint-disable-line no-param-reassign -- ignore
let firstToken = sourceCode.getFirstToken(node);
let lastToken = sourceCode.getLastToken(node);
if (!firstToken || !lastToken) {
return {
firstToken: firstToken || node,
lastToken: lastToken || node,
};
}
// Get the outermost left parenthesis if it's parenthesized.
let t = undefined;
let u = undefined;
while (
(t = sourceCode.getTokenBefore(firstToken)) &&
(u = sourceCode.getTokenAfter(lastToken)) &&
isLeftParen(t) &&
isRightParen(u) &&
t.range[0] >= borderOffset
) {
firstToken = t;
lastToken = u;
}
return { firstToken, lastToken };
}
/**
* Collect prefix tokens of the given property.
* The prefix includes `async`, `get`, `set`, `static`, and `*`.
* @param {Property|MethodDefinition} node The property node to collect prefix tokens.
* @returns {Array} tokens
*/
function getPrefixTokens(node) {
const prefixes = [];
let token = sourceCode.getFirstToken(node);
while (token != null && token.range[1] <= node.key.range[0]) {
prefixes.push(token);
token = sourceCode.getTokenAfter(token);
}
while (
isLeftParen(last(prefixes)) ||
isLeftBracket(last(prefixes))
) {
prefixes.pop();
}
return prefixes;
}
/**
* Check whether a given token is the first token of:
*
* - ExpressionStatement
* - A parameter of CallExpression/NewExpression
* - An element of ArrayExpression
* - An expression of SequenceExpression
*
* @param {Token} token The token to check.
* @param {Node} belongingNode The node that the token is belonging to.
* @returns {boolean} `true` if the token is the first token of an element.
*/
function isBeginningOfElement(token, belongingNode) {
let node = belongingNode;
while (node) {
const parent = node.parent;
const type = (parent && parent.type) || "";
if (
type.endsWith("Statement") ||
type.endsWith("Declaration")
) {
return parent.range[0] === token.range[0];
}
if (type === "CallExpression" || type === "NewExpression") {
const openParen = sourceCode.getTokenAfter(
parent.callee,
isNotRightParen,
);
return parent.arguments.some(
(param) =>
getFirstAndLastTokens(param, openParen.range[1])
.firstToken.range[0] === token.range[0],
);
}
if (type === "ArrayExpression") {
return parent.elements.some(
(element) =>
element &&
getFirstAndLastTokens(element).firstToken
.range[0] === token.range[0],
);
}
if (type === "SequenceExpression") {
return parent.expressions.some(
(expr) =>
getFirstAndLastTokens(expr).firstToken.range[0] ===
token.range[0],
);
}
node = parent;
}
return false;
}
/**
* Process the given node as body.
* The body node maybe a block statement or an expression node.
* @param {Node} node The body node to process.
* @param {Token} baseToken The base token.
* @returns {void}
*/
function processMaybeBlock(node, baseToken) {
const firstToken = getFirstAndLastTokens(node).firstToken;
setOffsetToToken(
firstToken,
isLeftBrace(firstToken) ? 0 : 1,
baseToken,
);
}
/**
* Collect nodeList.
* @param {Node[]} nodeList The node to process.
* @param {Node|null} leftToken The left parenthesis token.
* @param {Node|null} rightToken The right parenthesis token.
* @returns {void}
*/
function* genCollectNodeList(nodeList, leftToken, rightToken) {
let lastToken = leftToken;
/**
* Collect related tokens.
* Commas between this and the previous, and the first token of this node.
* @param {Token} firstToken The first token
* @returns {Array} Collect related tokens.
*/
function* genCollectTokens(firstToken) {
if (lastToken) {
let t = lastToken;
while (
(t = sourceCode.getTokenAfter(t, {
includeComments: true,
})) &&
t.range[1] <= firstToken.range[0]
) {
yield t;
}
}
}
for (const node of nodeList) {
if (!node) {
// Holes of an array.
continue;
}
const elementTokens = getFirstAndLastTokens(
node,
lastToken ? lastToken.range[1] : 0,
);
// Collect related tokens.
// Commas between this and the previous, and the first token of this node.
yield* genCollectTokens(elementTokens.firstToken);
yield elementTokens.firstToken;
// Save the last token to find tokens between the next token.
// eslint-disable-next-line require-atomic-updates -- ignore
lastToken = elementTokens.lastToken;
}
// Check trailing commas.
if (rightToken) {
yield* genCollectTokens(rightToken);
}
}
/**
* Set offset the given node list.
* @param {Node[]} nodeList The node to process.
* @param {Node|null} leftToken The left parenthesis token.
* @param {number} offset The offset to set.
* @param {Token} baseToken The token of the base offset.
* @returns {void}
*/
function setOffsetToNodeList(nodeList, leftToken, offset, baseToken) {
for (const t of genCollectNodeList(nodeList, leftToken, null)) {
setOffsetToToken(t, offset, baseToken);
}
}
/**
* Process the given node list.
* The first node is offsetted from the given left token.
* Rest nodes are adjusted to the first node.
* @param {Node[]} nodeList The node to process.
* @param {Node|null} leftToken The left parenthesis token.
* @param {Node|null} rightToken The right parenthesis token.
* @param {number} offset The offset to set.
* @returns {void}
*/
function processNodeList(nodeList, leftToken, rightToken, offset) {
if (nodeList.length >= 1) {
const alignTokens = Array.from(
genCollectNodeList(nodeList, leftToken, rightToken),
);
// Set offsets.
const baseToken = alignTokens.shift();
if (baseToken) {
// Set offset to the first token.
if (leftToken) {
setOffsetToToken(baseToken, offset, leftToken);
// Align tokens relatively to the left token.
setOffsetToToken(alignTokens, offset, leftToken);
} else {
// Align the rest tokens to the first token.
setOffsetToToken(alignTokens, 0, baseToken);
}
}
}
if (rightToken) {
setOffsetToToken(rightToken, 0, leftToken);
}
}
/**
* Validate indentation.
* @returns {void}
*/
function validateIndent() {
/**
* Check whether the line start is in template tag.
* @param {number} line The line.
* @returns {boolean} `true` if the line start is in template tag.
*/
function inTemplateTag(line) {
if (line <= 1) {
return false;
}
const lineStartIndex = sourceCode.getIndexFromLoc({
line,
column: 0,
});
return microTemplateService.inTemplateTag(lineStartIndex - 1);
}
/**
* Define the function which fixes the problem.
* @param {number} line The number of line.
* @param {string} actualText The actual indentation text.
* @param {ExpectedIndent} expectedIndent The expected indentation info.
* @returns {function} The defined function.
*/
function defineFix(line, actualText, expectedIndent) {
return (fixer) => {
const start = sourceCode.getIndexFromLoc({
line,
column: 0,
});
const indent = expectedIndent.indentText;
return fixer.replaceTextRange(
[start, start + actualText.length],
indent,
);
};
}
/**
* Validate base point indentation.
* @param {number} line The number of line.
* @param {string} actualText The actual indentation text.
* @param {ExpectedIndent} expectedIndent The expected indent.
* @returns {void}
*/
function validateBaseIndent(line, actualText, expectedIndent) {
const expectedBaseIndentText = expectedIndent.baseIndentText;
if (
expectedBaseIndentText &&
(actualText.length < expectedBaseIndentText.length ||
!actualText.startsWith(expectedBaseIndentText))
) {
context.report({
loc: {
start: { line, column: 0 },
end: {
line,
column: actualText.length,
},
},
messageId: actualText
? "unexpectedBaseIndentation"
: "missingBaseIndentation",
data: {
expected: JSON.stringify(expectedBaseIndentText),
actual: actualText
? JSON.stringify(
actualText.slice(
0,
expectedBaseIndentText.length,
),
)
: "",
},
fix: defineFix(line, actualText, expectedIndent),
});
return false;
}
return true;
}
offsets.forEach((value, line) => {
if (!inTemplateTag(line)) {
return;
}
if (!getLineText(line).trim()) {
// empty line
return;
}
const actualText = value.actualText;
const expectedIndent = getExpectedIndent(line);
if (!expectedIndent) {
return;
}
if (!validateBaseIndent(line, actualText, expectedIndent)) {
return;
}
const expectedBaseIndent = expectedIndent.baseIndentText.length;
const actualRelativeText = actualText.slice(expectedBaseIndent);
for (let i = 0; i < actualRelativeText.length; ++i) {
if (actualRelativeText[i] !== options.indentChar) {
context.report({
loc: {
start: { line, column: expectedBaseIndent + i },
end: {
line,
column: expectedBaseIndent + i + 1,
},
},
messageId: "unexpectedIndentationCharacter",
data: {
expected: JSON.stringify(options.indentChar),
actual: JSON.stringify(actualRelativeText[i]),
},
fix: defineFix(line, actualText, expectedIndent),
});
return;
}
}
const actualRelativeIndent = actualRelativeText.length;
if (actualRelativeIndent !== expectedIndent.indent) {
context.report({
loc: {
start: { line, column: expectedBaseIndent },
end: { line, column: actualText.length },
},
messageId: expectedBaseIndent
? "unexpectedRelativeIndentation"
: "unexpectedIndentation",
data: {
expectedIndent: expectedIndent.indent,
actualIndent: actualRelativeIndent,
unit,
expectedIndentPlural:
expectedIndent.indent === 1 ? "" : "s",
actualIndentPlural:
actualRelativeIndent === 1 ? "" : "s",
},
fix: defineFix(line, actualText, expectedIndent),
});
}
});
}
return {
"ArrayExpression, ArrayPattern"(node) {
processNodeList(
node.elements,
sourceCode.getFirstToken(node),
sourceCode.getLastToken(node),
1,
);
},
ArrowFunctionExpression(node) {
const firstToken = sourceCode.getFirstToken(node);
const secondToken = sourceCode.getTokenAfter(firstToken);
const leftToken = node.async ? secondToken : firstToken;
const arrowToken = sourceCode.getTokenBefore(
node.body,
isArrow,
);
if (node.async) {
// `async`
setOffsetToToken(secondToken, 1, firstToken);
}
if (isLeftParen(leftToken)) {
const rightToken = sourceCode.getTokenAfter(
last(node.params) || leftToken,
isRightParen,
);
// `(param1, param2)`
processNodeList(node.params, leftToken, rightToken, 1);
}
// `=>`
setOffsetToToken(arrowToken, 1, firstToken);
// body
processMaybeBlock(node.body, firstToken);
},
"AssignmentExpression, AssignmentPattern, BinaryExpression, LogicalExpression"(
node,
) {
const leftToken = getChainHeadToken(node);
const opToken = sourceCode.getTokenAfter(
node.left,
isNotRightParen,
);
const rightToken = sourceCode.getTokenAfter(opToken);
const prevToken = sourceCode.getTokenBefore(leftToken);
const shouldIndent =
!prevToken ||
prevToken.loc.end.line === leftToken.loc.start.line ||
isBeginningOfElement(leftToken, node);
setOffsetToToken(
// `= right`
[opToken, rightToken],
shouldIndent ? 1 : 0,
leftToken,
);
},
"AwaitExpression, RestElement, SpreadElement, UnaryExpression"(
node,
) {
const firstToken = sourceCode.getFirstToken(node);
const nextToken = sourceCode.getTokenAfter(firstToken);
setOffsetToToken(nextToken, 1, firstToken);
},
"BlockStatement, ClassBody"(node) {
processNodeList(
node.body,
sourceCode.getFirstToken(node),
sourceCode.getLastToken(node),
1,
);
},
"BreakStatement, ContinueStatement, ReturnStatement, ThrowStatement"(
node,
) {
if (node.argument || node.label) {
const firstToken = sourceCode.getFirstToken(node);
const nextToken = sourceCode.getTokenAfter(firstToken);
setOffsetToToken(nextToken, 1, firstToken);
}
},
CallExpression(node) {
const firstToken = sourceCode.getFirstToken(node);
const rightToken = sourceCode.getLastToken(node);
const leftToken = sourceCode.getTokenAfter(
node.callee,
isLeftParen,
);
// `(`
setOffsetToToken(leftToken, 1, firstToken);
// `(param1, param2)`
processNodeList(node.arguments, leftToken, rightToken, 1);
},
CatchClause(node) {
const firstToken = sourceCode.getFirstToken(node);
const bodyToken = sourceCode.getFirstToken(node.body);
if (node.param) {
const leftToken = sourceCode.getTokenAfter(firstToken);
const rightToken = sourceCode.getTokenAfter(node.param);
setOffsetToToken(leftToken, 1, firstToken);
processNodeList([node.param], leftToken, rightToken, 1);
}
setOffsetToToken(bodyToken, 0, firstToken);
},
"ClassDeclaration, ClassExpression"(node) {
const firstToken = sourceCode.getFirstToken(node);
const bodyToken = sourceCode.getFirstToken(node.body);
if (node.id) {
setOffsetToToken(
sourceCode.getFirstToken(node.id),
1,
firstToken,
);
}
if (node.superClass) {
const extendsToken = sourceCode.getTokenAfter(
node.id || firstToken,
);
const superClassToken =
sourceCode.getTokenAfter(extendsToken);
setOffsetToToken(extendsToken, 1, firstToken);
setOffsetToToken(superClassToken, 1, extendsToken);
}
setOffsetToToken(bodyToken, 0, firstToken);
},
ConditionalExpression(node) {
const firstToken = sourceCode.getFirstToken(node);
const questionToken = sourceCode.getTokenAfter(
node.test,
isNotRightParen,
);
const consequentToken = sourceCode.getTokenAfter(questionToken);
const colonToken = sourceCode.getTokenAfter(
node.consequent,
isNotRightParen,
);
const alternateToken = sourceCode.getTokenAfter(colonToken);
const isFlat =
node.test.loc.end.line === node.consequent.loc.start.line;
if (isFlat) {
setOffsetToToken(
[
questionToken,
consequentToken,
colonToken,
alternateToken,
],
0,
firstToken,
);
} else {
setOffsetToToken(
[questionToken, colonToken],
1,
firstToken,
);
setOffsetToToken(
[consequentToken, alternateToken],
1,
questionToken,
);
}
},
DoWhileStatement(node) {
const doToken = sourceCode.getFirstToken(node);
const whileToken = sourceCode.getTokenAfter(
node.body,
isNotRightParen,
);
const leftToken = sourceCode.getTokenAfter(whileToken);
const testToken = sourceCode.getTokenAfter(leftToken);
const lastToken = sourceCode.getLastToken(node);
const rightToken = isSemicolon(lastToken)
? sourceCode.getTokenBefore(lastToken)
: lastToken;
processMaybeBlock(node.body, doToken);
setOffsetToToken(whileToken, 0, doToken);
setOffsetToToken(leftToken, 1, whileToken);
setOffsetToToken(testToken, 1, leftToken);
setOffsetToToken(rightToken, 0, leftToken);
},
ExportAllDeclaration(node) {
const tokens = sourceCode.getTokens(node);
const firstToken = tokens.shift();
if (isSemicolon(last(tokens))) {
tokens.pop();
}
setOffsetToToken(tokens, 1, firstToken);
},
ExportDefaultDeclaration(node) {
const exportToken = sourceCode.getFirstToken(node);
const defaultToken = sourceCode.getFirstToken(node, 1);
const declarationToken = getFirstAndLastTokens(
node.declaration,
).firstToken;
setOffsetToToken(
[defaultToken, declarationToken],
1,
exportToken,
);
},
ExportNamedDeclaration(node) {
const exportToken = sourceCode.getFirstToken(node);
if (node.declaration) {
// export var foo = 1;
const declarationToken = sourceCode.getFirstToken(node, 1);
setOffsetToToken(declarationToken, 1, exportToken);
} else {
// export {foo, bar}; or export {foo, bar} from "mod";
const leftParenToken = sourceCode.getFirstToken(node, 1);
const rightParenToken = sourceCode.getLastToken(
node,
isRightBrace,
);
setOffsetToToken(leftParenToken, 0, exportToken);
processNodeList(
node.specifiers,
leftParenToken,
rightParenToken,
1,
);
const maybeFromToken =
sourceCode.getTokenAfter(rightParenToken);
if (
maybeFromToken &&
sourceCode.getText(maybeFromToken) === "from"
) {
const fromToken = maybeFromToken;
const nameToken = sourceCode.getTokenAfter(fromToken);
setOffsetToToken(
[fromToken, nameToken],
1,
exportToken,
);
}
}
},
ExportSpecifier(node) {
const tokens = sourceCode.getTokens(node);
const firstToken = tokens.shift();
setOffsetToToken(tokens, 1, firstToken);
},
"ForInStatement, ForOfStatement"(node) {
const forToken = sourceCode.getFirstToken(node);
const leftParenToken = sourceCode.getTokenAfter(forToken);
const leftToken = sourceCode.getTokenAfter(leftParenToken);
const inToken = sourceCode.getTokenAfter(
leftToken,
isNotRightParen,
);
const rightToken = sourceCode.getTokenAfter(inToken);
const rightParenToken = sourceCode.getTokenBefore(
node.body,
isNotLeftParen,
);
// `(`
setOffsetToToken(leftParenToken, 1, forToken);
// `const e`
setOffsetToToken(leftToken, 1, leftParenToken);
// `in` or `of`
setOffsetToToken(inToken, 1, leftToken);
// `array`
setOffsetToToken(rightToken, 1, leftToken);
// `)`
setOffsetToToken(rightParenToken, 0, leftParenToken);
processMaybeBlock(node.body, forToken);
},
ForStatement(node) {
const forToken = sourceCode.getFirstToken(node);
const leftParenToken = sourceCode.getTokenAfter(forToken);
const rightParenToken = sourceCode.getTokenBefore(
node.body,
isNotLeftParen,
);
// `(`
setOffsetToToken(leftParenToken, 1, forToken);
processNodeList(
// `let i = 0` `i < length` `i++`
[node.init, node.test, node.update],
// `(`
leftParenToken,
// `)`
rightParenToken,
1,
);
setOffsetToToken(rightParenToken, 0, leftParenToken);
processMaybeBlock(node.body, forToken);
},
"FunctionDeclaration, FunctionExpression"(node) {
const firstToken = sourceCode.getFirstToken(node);
if (isLeftParen(firstToken)) {
// Methods.
const leftToken = firstToken;
const rightToken = sourceCode.getTokenAfter(
last(node.params) || leftToken,
isRightParen,
);
const bodyToken = sourceCode.getFirstToken(node.body);
processNodeList(node.params, leftToken, rightToken, 1);
setOffsetToToken(
bodyToken,
0,
sourceCode.getFirstToken(node.parent),
);
} else {
// Normal functions.
const functionToken = node.async
? sourceCode.getTokenAfter(firstToken)
: firstToken;
const starToken = node.generator
? sourceCode.getTokenAfter(functionToken)
: null;
const idToken =
node.id && sourceCode.getFirstToken(node.id);
const leftToken = sourceCode.getTokenAfter(
idToken || starToken || functionToken,
);
const rightToken = sourceCode.getTokenAfter(
last(node.params) || leftToken,
isRightParen,
);
const bodyToken = sourceCode.getFirstToken(node.body);
if (node.async) {
setOffsetToToken(functionToken, 0, firstToken);
}
if (node.generator) {
setOffsetToToken(starToken, 1, firstToken);
}
if (node.id) {
setOffsetToToken(idToken, 1, firstToken);
}
setOffsetToToken(leftToken, 1, firstToken);
processNodeList(node.params, leftToken, rightToken, 1);
setOffsetToToken(bodyToken, 0, firstToken);
}
},
IfStatement(node) {
const ifToken = sourceCode.getFirstToken(node);
const ifLeftParenToken = sourceCode.getTokenAfter(ifToken);
const ifRightParenToken = sourceCode.getTokenBefore(
node.consequent,
isRightParen,
);
// `(`
setOffsetToToken(ifLeftParenToken, 1, ifToken);
// `)`
setOffsetToToken(ifRightParenToken, 0, ifLeftParenToken);
processMaybeBlock(node.consequent, ifToken);
if (node.alternate) {
// `else`
const elseToken = sourceCode.getTokenAfter(
node.consequent,
isNotRightParen,
);
setOffsetToToken(elseToken, 0, ifToken);
processMaybeBlock(node.alternate, elseToken);
}
},
ImportDeclaration(node) {
const firstSpecifier = node.specifiers[0];
const secondSpecifier = node.specifiers[1];
const importToken = sourceCode.getFirstToken(node);
const hasSemi = sourceCode.getLastToken(node).value === ";";
const tokens = []; // tokens to one indent
/**
* Process when the specifier does not exist
* @returns {void}
*/
function processNonSpecifier() {
const secondToken = sourceCode.getFirstToken(node, 1);
if (isLeftBrace(secondToken)) {
setOffsetToToken(
[
secondToken,
sourceCode.getTokenAfter(secondToken),
],
0,
importToken,
);
tokens.push(
sourceCode.getLastToken(node, hasSemi ? 2 : 1), // from
sourceCode.getLastToken(node, hasSemi ? 1 : 0), // "foo"
);
} else {
tokens.push(
sourceCode.getLastToken(node, hasSemi ? 1 : 0),
);
}
}
/**
* Process when the specifier is ImportDefaultSpecifier
* @returns {void}
*/
function processImportDefaultSpecifier() {
if (
secondSpecifier &&
secondSpecifier.type === "ImportNamespaceSpecifier"
) {
// There is a pattern:
// import Foo, * as foo from "foo"
tokens.push(
sourceCode.getFirstToken(firstSpecifier), // Foo
sourceCode.getTokenAfter(firstSpecifier), // comma
sourceCode.getFirstToken(secondSpecifier), // *
sourceCode.getLastToken(node, hasSemi ? 2 : 1), // from
sourceCode.getLastToken(node, hasSemi ? 1 : 0), // "foo"
);
} else {
// There are 3 patterns:
// import Foo from "foo"
// import Foo, {} from "foo"
// import Foo, {a} from "foo"
const idToken =
sourceCode.getFirstToken(firstSpecifier);
const nextToken =
sourceCode.getTokenAfter(firstSpecifier);
if (isComma(nextToken)) {
const leftBrace =
sourceCode.getTokenAfter(nextToken);
const rightBrace = sourceCode.getLastToken(
node,
hasSemi ? 3 : 2,
);
setOffsetToToken(
[idToken, nextToken],
1,
importToken,
);
setOffsetToToken(leftBrace, 0, idToken);
processNodeList(
node.specifiers.slice(1),
leftBrace,
rightBrace,
1,
);
tokens.push(
sourceCode.getLastToken(node, hasSemi ? 2 : 1), // from
sourceCode.getLastToken(node, hasSemi ? 1 : 0), // "foo"
);
} else {
tokens.push(
idToken,
nextToken, // from
sourceCode.getTokenAfter(nextToken), // "foo"
);
}
}
}
/**
* Process when the specifier is ImportNamespaceSpecifier
* @returns {void}
*/
function processImportNamespaceSpecifier() {
tokens.push(
sourceCode.getFirstToken(firstSpecifier), // *
sourceCode.getLastToken(node, hasSemi ? 2 : 1), // from
sourceCode.getLastToken(node, hasSemi ? 1 : 0), // "foo"
);
}
/**
* Process when the specifier is other
* @returns {void}
*/
function processOtherSpecifier() {
const leftBrace = sourceCode.getFirstToken(node, 1);
const rightBrace = sourceCode.getLastToken(
node,
hasSemi ? 3 : 2,
);
setOffsetToToken(leftBrace, 0, importToken);
processNodeList(node.specifiers, leftBrace, rightBrace, 1);
tokens.push(
sourceCode.getLastToken(node, hasSemi ? 2 : 1), // from
sourceCode.getLastToken(node, hasSemi ? 1 : 0), // "foo"
);
}
if (!firstSpecifier) {
// There are 2 patterns:
// import "foo"
// import {} from "foo"
processNonSpecifier();
} else if (firstSpecifier.type === "ImportDefaultSpecifier") {
processImportDefaultSpecifier();
} else if (firstSpecifier.type === "ImportNamespaceSpecifier") {
// There is a pattern:
// import * as foo from "foo"
processImportNamespaceSpecifier();
} else {
// There is a pattern:
// import {a} from "foo"
processOtherSpecifier();
}
setOffsetToToken(tokens, 1, importToken