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.
204 lines
5.6 KiB
204 lines
5.6 KiB
/** |
|
* @author Yosuke Ota |
|
*/ |
|
'use strict' |
|
|
|
const { isParenthesized } = require('eslint-utils') |
|
const { wrapCoreRule } = require('../utils') |
|
const { getStyleVariablesContext } = require('../utils/style-variables') |
|
|
|
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories |
|
module.exports = wrapCoreRule('no-extra-parens', { |
|
skipDynamicArguments: true, |
|
applyDocument: true, |
|
create: createForVueSyntax |
|
}) |
|
|
|
/** |
|
* 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.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.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.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.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.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.type === 'Punctuator' && token.value === ']' |
|
} |
|
|
|
/** |
|
* Determines if a given expression node is an IIFE |
|
* @param {Expression} node The node to check |
|
* @returns {node is CallExpression & { callee: FunctionExpression } } `true` if the given node is an IIFE |
|
*/ |
|
function isIIFE(node) { |
|
return ( |
|
node.type === 'CallExpression' && node.callee.type === 'FunctionExpression' |
|
) |
|
} |
|
|
|
/** |
|
* @param {RuleContext} context - The rule context. |
|
* @returns {TemplateListener} AST event handlers. |
|
*/ |
|
function createForVueSyntax(context) { |
|
if (!context.parserServices.getTemplateBodyTokenStore) { |
|
return {} |
|
} |
|
const tokenStore = context.parserServices.getTemplateBodyTokenStore() |
|
|
|
/** |
|
* Checks if the given node turns into a filter when unwraped. |
|
* @param {Expression} expression node to evaluate |
|
* @returns {boolean} `true` if the given node turns into a filter when unwraped. |
|
*/ |
|
function isUnwrapChangeToFilter(expression) { |
|
let parenStack = null |
|
for (const token of tokenStore.getTokens(expression)) { |
|
if (!parenStack) { |
|
if (token.value === '|') { |
|
return true |
|
} |
|
} else { |
|
if (parenStack.isUpToken(token)) { |
|
parenStack = parenStack.upper |
|
continue |
|
} |
|
} |
|
if (isLeftParen(token)) { |
|
parenStack = { isUpToken: isRightParen, upper: parenStack } |
|
} else if (isLeftBracket(token)) { |
|
parenStack = { isUpToken: isRightBracket, upper: parenStack } |
|
} else if (isLeftBrace(token)) { |
|
parenStack = { isUpToken: isRightBrace, upper: parenStack } |
|
} |
|
} |
|
return false |
|
} |
|
/** |
|
* Checks if the given node is CSS v-bind() without quote. |
|
* @param {VExpressionContainer} node |
|
* @param {Expression} expression |
|
*/ |
|
function isStyleVariableWithoutQuote(node, expression) { |
|
const styleVars = getStyleVariablesContext(context) |
|
if (!styleVars || !styleVars.vBinds.includes(node)) { |
|
return false |
|
} |
|
|
|
const vBindToken = tokenStore.getFirstToken(node) |
|
const tokens = tokenStore.getTokensBetween(vBindToken, expression) |
|
|
|
return tokens.every(isLeftParen) |
|
} |
|
/** |
|
* @param {VExpressionContainer & { expression: Expression | VFilterSequenceExpression | null }} node |
|
*/ |
|
function verify(node) { |
|
if (!node.expression) { |
|
return |
|
} |
|
|
|
const expression = |
|
node.expression.type === 'VFilterSequenceExpression' |
|
? node.expression.expression |
|
: node.expression |
|
|
|
if (!isParenthesized(expression, tokenStore)) { |
|
return |
|
} |
|
|
|
if (!isParenthesized(2, expression, tokenStore)) { |
|
if ( |
|
isIIFE(expression) && |
|
!isParenthesized(expression.callee, tokenStore) |
|
) { |
|
return |
|
} |
|
if (isUnwrapChangeToFilter(expression)) { |
|
return |
|
} |
|
if (isStyleVariableWithoutQuote(node, expression)) { |
|
return |
|
} |
|
} |
|
report(expression) |
|
} |
|
|
|
/** |
|
* Report the node |
|
* @param {Expression} node node to evaluate |
|
* @returns {void} |
|
* @private |
|
*/ |
|
function report(node) { |
|
const sourceCode = context.getSourceCode() |
|
const leftParenToken = tokenStore.getTokenBefore(node) |
|
const rightParenToken = tokenStore.getTokenAfter(node) |
|
|
|
context.report({ |
|
node, |
|
loc: leftParenToken.loc, |
|
messageId: 'unexpected', |
|
fix(fixer) { |
|
const parenthesizedSource = sourceCode.text.slice( |
|
leftParenToken.range[1], |
|
rightParenToken.range[0] |
|
) |
|
|
|
return fixer.replaceTextRange( |
|
[leftParenToken.range[0], rightParenToken.range[1]], |
|
parenthesizedSource |
|
) |
|
} |
|
}) |
|
} |
|
|
|
return { |
|
"VAttribute[directive=true][key.name.name='bind'] > VExpressionContainer": |
|
verify, |
|
'VElement > VExpressionContainer': verify |
|
} |
|
}
|
|
|