eslint-plugin-vue
Advanced tools
Comparing version 7.1.0 to 7.2.0
@@ -9,3 +9,2 @@ /* | ||
rules: { | ||
'vue/custom-event-name-casing': 'error', | ||
'vue/no-arrow-functions-in-watch': 'error', | ||
@@ -12,0 +11,0 @@ 'vue/no-async-in-computed-properties': 'error', |
@@ -9,3 +9,2 @@ /* | ||
rules: { | ||
'vue/custom-event-name-casing': 'error', | ||
'vue/no-arrow-functions-in-watch': 'error', | ||
@@ -12,0 +11,0 @@ 'vue/no-async-in-computed-properties': 'error', |
@@ -13,3 +13,3 @@ /** | ||
const utils = require('../utils') | ||
const { isKebabCase } = require('../utils/casing') | ||
const casing = require('../utils/casing') | ||
const { toRegExp } = require('../utils/regexp') | ||
@@ -21,10 +21,4 @@ | ||
/** | ||
* Check whether the given event name is valid. | ||
* @param {string} name The name to check. | ||
* @returns {boolean} `true` if the given event name is valid. | ||
*/ | ||
function isValidEventName(name) { | ||
return isKebabCase(name) || name.startsWith('update:') | ||
} | ||
const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase'] | ||
const DEFAULT_CASE = 'kebab-case' | ||
@@ -69,2 +63,14 @@ /** | ||
const OBJECT_OPTION_SCHEMA = { | ||
type: 'object', | ||
properties: { | ||
ignores: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
uniqueItems: true, | ||
additionalItems: false | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
module.exports = { | ||
@@ -74,23 +80,27 @@ meta: { | ||
docs: { | ||
description: 'enforce custom event names always use "kebab-case"', | ||
categories: ['vue3-essential', 'essential'], | ||
description: 'enforce specific casing for custom event name', | ||
categories: undefined, | ||
url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html' | ||
}, | ||
fixable: null, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignores: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
uniqueItems: true, | ||
additionalItems: false | ||
} | ||
schema: { | ||
anyOf: [ | ||
{ | ||
type: 'array', | ||
items: [ | ||
{ | ||
enum: ALLOWED_CASE_OPTIONS | ||
}, | ||
OBJECT_OPTION_SCHEMA | ||
] | ||
}, | ||
additionalProperties: false | ||
} | ||
], | ||
// For backward compatibility | ||
{ | ||
type: 'array', | ||
items: [OBJECT_OPTION_SCHEMA] | ||
} | ||
] | ||
}, | ||
messages: { | ||
unexpected: "Custom event name '{{name}}' must be kebab-case." | ||
unexpected: "Custom event name '{{name}}' must be {{caseType}}." | ||
} | ||
@@ -100,8 +110,25 @@ }, | ||
create(context) { | ||
/** @type {Map<ObjectExpression, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */ | ||
const setupContexts = new Map() | ||
const options = context.options[0] || {} | ||
const options = | ||
context.options.length === 1 && typeof context.options[0] !== 'string' | ||
? // For backward compatibility | ||
[undefined, context.options[0]] | ||
: context.options | ||
const caseType = options[0] || DEFAULT_CASE | ||
const objectOption = options[1] || {} | ||
const caseChecker = casing.getChecker(caseType) | ||
/** @type {RegExp[]} */ | ||
const ignores = (options.ignores || []).map(toRegExp) | ||
const ignores = (objectOption.ignores || []).map(toRegExp) | ||
/** | ||
* Check whether the given event name is valid. | ||
* @param {string} name The name to check. | ||
* @returns {boolean} `true` if the given event name is valid. | ||
*/ | ||
function isValidEventName(name) { | ||
return caseChecker(name) || name.startsWith('update:') | ||
} | ||
/** | ||
* @param { Literal & { value: string } } nameLiteralNode | ||
@@ -111,3 +138,3 @@ */ | ||
const name = nameLiteralNode.value | ||
if (ignores.some((re) => re.test(name)) || isValidEventName(name)) { | ||
if (isValidEventName(name) || ignores.some((re) => re.test(name))) { | ||
return | ||
@@ -119,3 +146,4 @@ } | ||
data: { | ||
name | ||
name, | ||
caseType | ||
} | ||
@@ -200,3 +228,6 @@ }) | ||
const { contextReferenceIds, emitReferenceIds } = setupContext | ||
if (emitReferenceIds.has(node.callee)) { | ||
if ( | ||
node.callee.type === 'Identifier' && | ||
emitReferenceIds.has(node.callee) | ||
) { | ||
// verify setup(props,{emit}) {emit()} | ||
@@ -209,2 +240,3 @@ verify(nameLiteralNode) | ||
emit.name === 'emit' && | ||
emit.member.object.type === 'Identifier' && | ||
contextReferenceIds.has(emit.member.object) | ||
@@ -211,0 +243,0 @@ ) { |
@@ -31,11 +31,15 @@ /** | ||
for (const property of watchValue.properties) { | ||
if ( | ||
property.type === 'Property' && | ||
property.value.type === 'ArrowFunctionExpression' | ||
) { | ||
context.report({ | ||
node: property, | ||
message: 'You should not use an arrow function to define a watcher.' | ||
}) | ||
if (property.type !== 'Property') { | ||
continue | ||
} | ||
for (const handler of utils.iterateWatchHandlerValues(property)) { | ||
if (handler.type === 'ArrowFunctionExpression') { | ||
context.report({ | ||
node: handler, | ||
message: | ||
'You should not use an arrow function to define a watcher.' | ||
}) | ||
} | ||
} | ||
} | ||
@@ -42,0 +46,0 @@ }) |
@@ -637,22 +637,15 @@ /** | ||
if (property.kind === 'init') { | ||
/** @type {Expression | null} */ | ||
let handlerValueNode = null | ||
if (property.value.type === 'ObjectExpression') { | ||
const handler = utils.findProperty(property.value, 'handler') | ||
if (handler) { | ||
handlerValueNode = handler.value | ||
for (const handlerValueNode of utils.iterateWatchHandlerValues( | ||
property | ||
)) { | ||
if ( | ||
handlerValueNode.type === 'Literal' || | ||
handlerValueNode.type === 'TemplateLiteral' | ||
) { | ||
const name = utils.getStringLiteralValue(handlerValueNode) | ||
if (name != null) { | ||
watcherUsedProperties.add(name) | ||
} | ||
} | ||
} else { | ||
handlerValueNode = property.value | ||
} | ||
if ( | ||
handlerValueNode && | ||
(handlerValueNode.type === 'Literal' || | ||
handlerValueNode.type === 'TemplateLiteral') | ||
) { | ||
const name = utils.getStringLiteralValue(handlerValueNode) | ||
if (name != null) { | ||
watcherUsedProperties.add(name) | ||
} | ||
} | ||
} | ||
@@ -703,7 +696,7 @@ } | ||
'computed' || | ||
utils.getStaticPropertyName(property) !== 'handler' | ||
utils.getStaticPropertyName(property) !== 'get' | ||
) { | ||
return | ||
} | ||
// check { computed: { foo: { handler: (vm) => vm.prop } } } | ||
// check { computed: { foo: { get: () => vm.prop } } } | ||
} else { | ||
@@ -710,0 +703,0 @@ return |
@@ -14,22 +14,2 @@ /** | ||
// ------------------------------------------------------------------------------ | ||
// Helpers | ||
// ------------------------------------------------------------------------------ | ||
/** | ||
* Check whether the given node is an well-known element or not. | ||
* @param {VElement} node The element node to check. | ||
* @returns {boolean} `true` if the name is an well-known element name. | ||
*/ | ||
function isWellKnownElement(node) { | ||
if ( | ||
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) || | ||
utils.isHtmlWellKnownElementName(node.rawName) || | ||
utils.isSvgWellKnownElementName(node.rawName) | ||
) { | ||
return true | ||
} | ||
return false | ||
} | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
@@ -65,5 +45,2 @@ // ------------------------------------------------------------------------------ | ||
} | ||
if (!isWellKnownElement(element)) { | ||
return | ||
} | ||
if ( | ||
@@ -70,0 +47,0 @@ !utils.hasDirective(element, 'if') && |
@@ -12,2 +12,6 @@ /** | ||
/** | ||
* @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
@@ -100,75 +104,108 @@ // Helpers | ||
return utils.defineTemplateBodyVisitor(context, { | ||
...(always | ||
? { | ||
/** @param {Identifier} node */ | ||
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"( | ||
node | ||
) { | ||
context.report({ | ||
node, | ||
message: | ||
"Method calls inside of 'v-on' directives must have parentheses." | ||
}) | ||
} | ||
if (always) { | ||
return utils.defineTemplateBodyVisitor(context, { | ||
/** @param {Identifier} node */ | ||
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"( | ||
node | ||
) { | ||
context.report({ | ||
node, | ||
message: | ||
"Method calls inside of 'v-on' directives must have parentheses." | ||
}) | ||
} | ||
}) | ||
} | ||
const option = context.options[1] || {} | ||
const ignoreIncludesComment = !!option.ignoreIncludesComment | ||
/** @type {Set<string>} */ | ||
const useArgsMethods = new Set() | ||
return utils.defineTemplateBodyVisitor( | ||
context, | ||
{ | ||
/** @param {VOnExpression} node */ | ||
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"( | ||
node | ||
) { | ||
const expression = getInvalidNeverCallExpression(node) | ||
if (!expression) { | ||
return | ||
} | ||
: { | ||
/** @param {VOnExpression} node */ | ||
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"( | ||
node | ||
) { | ||
const expression = getInvalidNeverCallExpression(node) | ||
if (!expression) { | ||
return | ||
} | ||
const option = context.options[1] || {} | ||
const ignoreIncludesComment = !!option.ignoreIncludesComment | ||
const tokenStore = context.parserServices.getTemplateBodyTokenStore() | ||
const tokens = tokenStore.getTokens(node.parent, { | ||
includeComments: true | ||
}) | ||
/** @type {Token | undefined} */ | ||
let leftQuote | ||
/** @type {Token | undefined} */ | ||
let rightQuote | ||
if (isQuote(tokens[0])) { | ||
leftQuote = tokens.shift() | ||
rightQuote = tokens.pop() | ||
} | ||
const tokenStore = context.parserServices.getTemplateBodyTokenStore() | ||
const tokens = tokenStore.getTokens(node.parent, { | ||
includeComments: true | ||
}) | ||
/** @type {Token | undefined} */ | ||
let leftQuote | ||
/** @type {Token | undefined} */ | ||
let rightQuote | ||
if (isQuote(tokens[0])) { | ||
leftQuote = tokens.shift() | ||
rightQuote = tokens.pop() | ||
} | ||
const hasComment = tokens.some( | ||
(token) => token.type === 'Block' || token.type === 'Line' | ||
) | ||
const hasComment = tokens.some( | ||
(token) => token.type === 'Block' || token.type === 'Line' | ||
) | ||
if (ignoreIncludesComment && hasComment) { | ||
return | ||
} | ||
if (ignoreIncludesComment && hasComment) { | ||
return | ||
} | ||
context.report({ | ||
node: expression, | ||
message: | ||
"Method calls without arguments inside of 'v-on' directives must not have parentheses.", | ||
fix: hasComment | ||
? null /* The comment is included and cannot be fixed. */ | ||
: (fixer) => { | ||
/** @type {Range} */ | ||
const range = | ||
leftQuote && rightQuote | ||
? [leftQuote.range[1], rightQuote.range[0]] | ||
: [ | ||
tokens[0].range[0], | ||
tokens[tokens.length - 1].range[1] | ||
] | ||
if (expression.callee.type === 'Identifier') { | ||
if (useArgsMethods.has(expression.callee.name)) { | ||
// The behavior of target method can change given the arguments. | ||
return | ||
} | ||
} | ||
return fixer.replaceTextRange( | ||
range, | ||
context.getSourceCode().getText(expression.callee) | ||
) | ||
} | ||
}) | ||
context.report({ | ||
node: expression, | ||
message: | ||
"Method calls without arguments inside of 'v-on' directives must not have parentheses.", | ||
fix: hasComment | ||
? null /* The comment is included and cannot be fixed. */ | ||
: (fixer) => { | ||
/** @type {Range} */ | ||
const range = | ||
leftQuote && rightQuote | ||
? [leftQuote.range[1], rightQuote.range[0]] | ||
: [tokens[0].range[0], tokens[tokens.length - 1].range[1]] | ||
return fixer.replaceTextRange( | ||
range, | ||
context.getSourceCode().getText(expression.callee) | ||
) | ||
} | ||
}) | ||
} | ||
}, | ||
utils.defineVueVisitor(context, { | ||
onVueObjectEnter(node) { | ||
for (const method of utils.iterateProperties( | ||
node, | ||
new Set(['methods']) | ||
)) { | ||
if (useArgsMethods.has(method.name)) { | ||
continue | ||
} | ||
}) | ||
}) | ||
if (method.type !== 'object') { | ||
continue | ||
} | ||
const value = method.property.value | ||
if ( | ||
(value.type === 'FunctionExpression' || | ||
value.type === 'ArrowFunctionExpression') && | ||
value.params.length > 0 | ||
) { | ||
useArgsMethods.add(method.name) | ||
} | ||
} | ||
} | ||
}) | ||
) | ||
} | ||
} |
@@ -23,7 +23,3 @@ /** | ||
function isValidElement(node) { | ||
if ( | ||
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) || | ||
utils.isHtmlWellKnownElementName(node.rawName) || | ||
utils.isSvgWellKnownElementName(node.rawName) | ||
) { | ||
if (!utils.isCustomComponent(node)) { | ||
// non Vue-component | ||
@@ -30,0 +26,0 @@ return false |
@@ -10,2 +10,6 @@ /** | ||
/** | ||
* @typedef { { expr: VForExpression, variables: VVariable[] } } VSlotVForVariables | ||
*/ | ||
/** | ||
* Get all `v-slot` directives on a given element. | ||
@@ -97,12 +101,36 @@ * @param {VElement} node The VElement node to check. | ||
* @param {VDirective} currentVSlot The current `v-slot` directive node. | ||
* @param {VSlotVForVariables | null} currentVSlotVForVars The current `v-for` variables. | ||
* @param {SourceCode} sourceCode The source code. | ||
* @param {ParserServices.TokenStore} tokenStore The token store. | ||
* @returns {VDirective[][]} The array of the group of `v-slot` directives. | ||
*/ | ||
function filterSameSlot(vSlotGroups, currentVSlot, sourceCode) { | ||
function filterSameSlot( | ||
vSlotGroups, | ||
currentVSlot, | ||
currentVSlotVForVars, | ||
sourceCode, | ||
tokenStore | ||
) { | ||
const currentName = getNormalizedName(currentVSlot, sourceCode) | ||
return vSlotGroups | ||
.map((vSlots) => | ||
vSlots.filter( | ||
(vSlot) => getNormalizedName(vSlot, sourceCode) === currentName | ||
) | ||
vSlots.filter((vSlot) => { | ||
if (getNormalizedName(vSlot, sourceCode) !== currentName) { | ||
return false | ||
} | ||
const vForExpr = getVSlotVForVariableIfUsingIterationVars( | ||
vSlot, | ||
utils.getDirective(vSlot.parent.parent, 'for') | ||
) | ||
if (!currentVSlotVForVars || !vForExpr) { | ||
return !currentVSlotVForVars && !vForExpr | ||
} | ||
if ( | ||
!equalVSlotVForVariables(currentVSlotVForVars, vForExpr, tokenStore) | ||
) { | ||
return false | ||
} | ||
// | ||
return true | ||
}) | ||
) | ||
@@ -113,8 +141,85 @@ .filter((slots) => slots.length >= 1) | ||
/** | ||
* Check whether a given argument node is using an iteration variable that the element defined. | ||
* Determines whether the two given `v-slot` variables are considered to be equal. | ||
* @param {VSlotVForVariables} a First element. | ||
* @param {VSlotVForVariables} b Second element. | ||
* @param {ParserServices.TokenStore} tokenStore The token store. | ||
* @returns {boolean} `true` if the elements are considered to be equal. | ||
*/ | ||
function equalVSlotVForVariables(a, b, tokenStore) { | ||
if (a.variables.length !== b.variables.length) { | ||
return false | ||
} | ||
if (!equal(a.expr.right, b.expr.right)) { | ||
return false | ||
} | ||
const checkedVarNames = new Set() | ||
const len = Math.min(a.expr.left.length, b.expr.left.length) | ||
for (let index = 0; index < len; index++) { | ||
const aPtn = a.expr.left[index] | ||
const bPtn = b.expr.left[index] | ||
const aVar = a.variables.find( | ||
(v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1] | ||
) | ||
const bVar = b.variables.find( | ||
(v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1] | ||
) | ||
if (aVar && bVar) { | ||
if (aVar.id.name !== bVar.id.name) { | ||
return false | ||
} | ||
if (!equal(aPtn, bPtn)) { | ||
return false | ||
} | ||
checkedVarNames.add(aVar.id.name) | ||
} else if (aVar || bVar) { | ||
return false | ||
} | ||
} | ||
for (const v of a.variables) { | ||
if (!checkedVarNames.has(v.id.name)) { | ||
if (b.variables.every((bv) => v.id.name !== bv.id.name)) { | ||
return false | ||
} | ||
} | ||
} | ||
return true | ||
/** | ||
* Determines whether the two given nodes are considered to be equal. | ||
* @param {ASTNode} a First node. | ||
* @param {ASTNode} b Second node. | ||
* @returns {boolean} `true` if the nodes are considered to be equal. | ||
*/ | ||
function equal(a, b) { | ||
if (a.type !== b.type) { | ||
return false | ||
} | ||
return utils.equalTokens(a, b, tokenStore) | ||
} | ||
} | ||
/** | ||
* Gets the `v-for` directive and variable that provide the variables used by the given` v-slot` directive. | ||
* @param {VDirective} vSlot The current `v-slot` directive node. | ||
* @param {VDirective | null} [vFor] The current `v-for` directive node. | ||
* @returns { VSlotVForVariables | null } The VSlotVForVariable. | ||
*/ | ||
function getVSlotVForVariableIfUsingIterationVars(vSlot, vFor) { | ||
const expr = | ||
vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression) | ||
const variables = | ||
expr && getUsingIterationVars(vSlot.key.argument, vSlot.parent.parent) | ||
return expr && variables && variables.length ? { expr, variables } : null | ||
} | ||
/** | ||
* Gets iterative variables if a given argument node is using iterative variables that the element defined. | ||
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check. | ||
* @param {VElement} element The element node which has the argument. | ||
* @returns {boolean} `true` if the argument node is using the iteration variable. | ||
* @returns {VVariable[]} The argument node is using iteration variables. | ||
*/ | ||
function isUsingIterationVar(argument, element) { | ||
function getUsingIterationVars(argument, element) { | ||
const vars = [] | ||
if (argument && argument.type === 'VExpressionContainer') { | ||
@@ -128,7 +233,7 @@ for (const { variable } of argument.references) { | ||
) { | ||
return true | ||
vars.push(variable) | ||
} | ||
} | ||
} | ||
return false | ||
return vars | ||
} | ||
@@ -213,2 +318,5 @@ | ||
const sourceCode = context.getSourceCode() | ||
const tokenStore = | ||
context.parserServices.getTemplateBodyTokenStore && | ||
context.parserServices.getTemplateBodyTokenStore() | ||
const options = context.options[0] || {} | ||
@@ -264,8 +372,14 @@ const allowModifiers = options.allowModifiers === true | ||
if (ownerElement === parentElement) { | ||
const vFor = utils.getDirective(element, 'for') | ||
const vSlotVForVar = getVSlotVForVariableIfUsingIterationVars( | ||
node, | ||
vFor | ||
) | ||
const vSlotGroupsOfSameSlot = filterSameSlot( | ||
vSlotGroupsOnChildren, | ||
node, | ||
sourceCode | ||
vSlotVForVar, | ||
sourceCode, | ||
tokenStore | ||
) | ||
const vFor = utils.getDirective(element, 'for') | ||
if ( | ||
@@ -282,3 +396,3 @@ vSlotGroupsOfSameSlot.length >= 2 && | ||
} | ||
if (vFor && !isUsingIterationVar(node.key.argument, element)) { | ||
if (vFor && !vSlotVForVar) { | ||
// E.g., <template v-for="x of xs" #one></template> | ||
@@ -285,0 +399,0 @@ context.report({ |
@@ -593,2 +593,4 @@ /** | ||
!this.isHtmlWellKnownElementName(node.rawName)) || | ||
(this.isSvgElementNode(node) && | ||
!this.isSvgWellKnownElementName(node.rawName)) || | ||
this.hasAttribute(node, 'is') || | ||
@@ -1479,2 +1481,9 @@ this.hasDirective(node, 'bind', 'is') || | ||
/** | ||
* Return generator with the all handler nodes defined in the given watcher property. | ||
* @param {Property|Expression} property | ||
* @returns {IterableIterator<Expression>} | ||
*/ | ||
iterateWatchHandlerValues, | ||
/** | ||
* Wraps composition API trace map in both 'vue' and '@vue/composition-api' imports | ||
@@ -1993,1 +2002,24 @@ * @param {import('eslint-utils').TYPES.TraceMap} map | ||
} | ||
/** | ||
* Return generator with the all handler nodes defined in the given watcher property. | ||
* @param {Property|Expression} property | ||
* @returns {IterableIterator<Expression>} | ||
*/ | ||
function* iterateWatchHandlerValues(property) { | ||
const value = property.type === 'Property' ? property.value : property | ||
if (value.type === 'ObjectExpression') { | ||
const handler = findProperty(value, 'handler') | ||
if (handler) { | ||
yield handler.value | ||
} | ||
} else if (value.type === 'ArrayExpression') { | ||
for (const element of value.elements.filter(isDef)) { | ||
if (element.type !== 'SpreadElement') { | ||
yield* iterateWatchHandlerValues(element) | ||
} | ||
} | ||
} else { | ||
yield value | ||
} | ||
} |
{ | ||
"name": "eslint-plugin-vue", | ||
"version": "7.1.0", | ||
"version": "7.2.0", | ||
"description": "Official ESLint plugin for Vue.js", | ||
@@ -58,3 +58,3 @@ "main": "lib/index.js", | ||
"semver": "^7.3.2", | ||
"vue-eslint-parser": "^7.1.1" | ||
"vue-eslint-parser": "^7.2.0" | ||
}, | ||
@@ -61,0 +61,0 @@ "devDependencies": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
766439
23634
Updatedvue-eslint-parser@^7.2.0