eslint-plugin-vue
Advanced tools
Comparing version 7.0.0-alpha.10 to 7.0.0-beta.0
@@ -9,3 +9,3 @@ /* | ||
parserOptions: { | ||
ecmaVersion: 2018, | ||
ecmaVersion: 2020, | ||
sourceType: 'module' | ||
@@ -12,0 +12,0 @@ }, |
@@ -63,2 +63,3 @@ /* | ||
'vue/valid-v-if': 'error', | ||
'vue/valid-v-is': 'error', | ||
'vue/valid-v-model': 'error', | ||
@@ -65,0 +66,0 @@ 'vue/valid-v-on': 'error', |
@@ -26,2 +26,3 @@ /* | ||
'vue/require-default-prop': 'warn', | ||
'vue/require-explicit-emits': 'warn', | ||
'vue/require-prop-types': 'warn', | ||
@@ -28,0 +29,0 @@ 'vue/singleline-html-element-content-newline': 'warn', |
@@ -161,2 +161,3 @@ /* | ||
'valid-v-if': require('./rules/valid-v-if'), | ||
'valid-v-is': require('./rules/valid-v-is'), | ||
'valid-v-model': require('./rules/valid-v-model'), | ||
@@ -163,0 +164,0 @@ 'valid-v-on': require('./rules/valid-v-on'), |
@@ -87,2 +87,4 @@ /** | ||
return ATTRS.UNIQUE | ||
} else if (name === 'is') { | ||
return ATTRS.DEFINITION | ||
} else { | ||
@@ -89,0 +91,0 @@ return ATTRS.OTHER_DIRECTIVES |
@@ -138,12 +138,9 @@ /** | ||
}, | ||
fix: (fixer) => { | ||
*fix(fixer) { | ||
yield fixer.replaceText(open, `<${casingName}`) | ||
const endTag = node.endTag | ||
if (!endTag) { | ||
return fixer.replaceText(open, `<${casingName}`) | ||
if (endTag) { | ||
const endTagOpen = tokens.getFirstToken(endTag) | ||
yield fixer.replaceText(endTagOpen, `</${casingName}`) | ||
} | ||
const endTagOpen = tokens.getFirstToken(endTag) | ||
return [ | ||
fixer.replaceText(open, `<${casingName}`), | ||
fixer.replaceText(endTagOpen, `</${casingName}`) | ||
] | ||
} | ||
@@ -150,0 +147,0 @@ }) |
@@ -51,3 +51,3 @@ /** | ||
function getCalleeMemberNode(node) { | ||
const callee = node.callee | ||
const callee = utils.skipChainExpression(node.callee) | ||
@@ -120,3 +120,3 @@ if (callee.type === 'MemberExpression') { | ||
onSetupFunctionEnter(node, { node: vueNode }) { | ||
const contextParam = utils.unwrapAssignmentPattern(node.params[1]) | ||
const contextParam = utils.skipDefaultParamValue(node.params[1]) | ||
if (!contextParam) { | ||
@@ -123,0 +123,0 @@ // no arguments |
@@ -166,3 +166,3 @@ /** | ||
}, | ||
fix: (fixer) => { | ||
fix(fixer) { | ||
const tokens = context.parserServices.getTemplateBodyTokenStore() | ||
@@ -191,3 +191,3 @@ const close = tokens.getLastToken(node.startTag) | ||
}, | ||
fix: (fixer) => { | ||
fix(fixer) { | ||
const tokens = context.parserServices.getTemplateBodyTokenStore() | ||
@@ -194,0 +194,0 @@ const close = tokens.getLastToken(node.startTag) |
@@ -29,12 +29,13 @@ /** | ||
function isTimedFunction(node) { | ||
const callee = utils.skipChainExpression(node.callee) | ||
return ( | ||
((node.type === 'CallExpression' && | ||
node.callee.type === 'Identifier' && | ||
TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1) || | ||
callee.type === 'Identifier' && | ||
TIMED_FUNCTIONS.indexOf(callee.name) !== -1) || | ||
(node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.object.type === 'Identifier' && | ||
node.callee.object.name === 'window' && | ||
node.callee.property.type === 'Identifier' && | ||
TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1)) && | ||
callee.type === 'MemberExpression' && | ||
callee.object.type === 'Identifier' && | ||
callee.object.name === 'window' && | ||
callee.property.type === 'Identifier' && | ||
TIMED_FUNCTIONS.indexOf(callee.property.name) !== -1)) && | ||
node.arguments.length | ||
@@ -48,14 +49,12 @@ ) | ||
function isPromise(node) { | ||
if ( | ||
node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' | ||
) { | ||
const callee = utils.skipChainExpression(node.callee) | ||
if (node.type === 'CallExpression' && callee.type === 'MemberExpression') { | ||
return ( | ||
// hello.PROMISE_FUNCTION() | ||
(node.callee.property.type === 'Identifier' && | ||
PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1) || // Promise.PROMISE_METHOD() | ||
(node.callee.object.type === 'Identifier' && | ||
node.callee.object.name === 'Promise' && | ||
node.callee.property.type === 'Identifier' && | ||
PROMISE_METHODS.indexOf(node.callee.property.name) !== -1) | ||
(callee.property.type === 'Identifier' && | ||
PROMISE_FUNCTIONS.indexOf(callee.property.name) !== -1) || // Promise.PROMISE_METHOD() | ||
(callee.object.type === 'Identifier' && | ||
callee.object.name === 'Promise' && | ||
callee.property.type === 'Identifier' && | ||
PROMISE_METHODS.indexOf(callee.property.name) !== -1) | ||
) | ||
@@ -62,0 +61,0 @@ } |
@@ -35,9 +35,22 @@ /** | ||
return utils.defineVueVisitor(context, { | ||
/** @param {MemberExpression & {parent: CallExpression}} node */ | ||
'CallExpression > MemberExpression'(node) { | ||
const call = node.parent | ||
/** @param {MemberExpression & ({parent: CallExpression} | {parent: ChainExpression & {parent: CallExpression}})} node */ | ||
'CallExpression > MemberExpression, CallExpression > ChainExpression > MemberExpression'( | ||
node | ||
) { | ||
const call = | ||
node.parent.type === 'ChainExpression' | ||
? node.parent.parent | ||
: node.parent | ||
if (call.optional) { | ||
// It is OK because checking whether it is deprecated. | ||
// e.g. `this.$on?.()` | ||
return | ||
} | ||
if ( | ||
call.callee !== node || | ||
node.property.type !== 'Identifier' || | ||
!['$on', '$off', '$once'].includes(node.property.name) | ||
utils.skipChainExpression(call.callee) !== node || | ||
!['$on', '$off', '$once'].includes( | ||
utils.getStaticPropertyName(node) || '' | ||
) | ||
) { | ||
@@ -44,0 +57,0 @@ return |
@@ -42,3 +42,3 @@ /** | ||
messageId: 'syncModifierIsDeprecated', | ||
fix: (fixer) => { | ||
fix(fixer) { | ||
if (node.key.argument == null) { | ||
@@ -45,0 +45,0 @@ // is using spread syntax |
@@ -50,3 +50,3 @@ /** | ||
messageId: 'numberModifierIsDeprecated', | ||
fix: (fixer) => { | ||
fix(fixer) { | ||
const key = keyCodeToKey[keyCodes] | ||
@@ -53,0 +53,0 @@ if (!key) return null |
@@ -7,2 +7,4 @@ /** | ||
const utils = require('../utils') | ||
// ------------------------------------------------------------------------------ | ||
@@ -35,3 +37,3 @@ // Rule Definition | ||
) { | ||
const config = node.object | ||
const config = utils.skipChainExpression(node.object) | ||
if ( | ||
@@ -38,0 +40,0 @@ config.type !== 'MemberExpression' || |
@@ -103,11 +103,8 @@ /** | ||
MemberExpression(node) { | ||
const object = node.object | ||
const object = utils.skipChainExpression(node.object) | ||
if (object.type !== 'MemberExpression') { | ||
return | ||
} | ||
if ( | ||
object.property.type !== 'Identifier' || | ||
(object.property.name !== '$slots' && | ||
object.property.name !== '$scopedSlots') | ||
) { | ||
const name = utils.getStaticPropertyName(object) | ||
if (!name || (name !== '$slots' && name !== '$scopedSlots')) { | ||
return | ||
@@ -114,0 +111,0 @@ } |
@@ -52,14 +52,14 @@ /** | ||
const rightNode = utils.skipChainExpression(right) | ||
if ( | ||
left.type !== 'ArrayPattern' && | ||
left.type !== 'ObjectPattern' && | ||
right.type !== 'MemberExpression' | ||
rightNode.type !== 'MemberExpression' | ||
) { | ||
return | ||
} | ||
/** @type {Expression | Super} */ | ||
let rightId = right | ||
let rightId = rightNode | ||
while (rightId.type === 'MemberExpression') { | ||
rightId = rightId.object | ||
rightId = utils.skipChainExpression(rightId.object) | ||
} | ||
@@ -88,3 +88,3 @@ if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) { | ||
onSetupFunctionEnter(node) { | ||
const propsParam = utils.unwrapAssignmentPattern(node.params[0]) | ||
const propsParam = utils.skipDefaultParamValue(node.params[0]) | ||
if (!propsParam) { | ||
@@ -91,0 +91,0 @@ // no arguments |
@@ -98,3 +98,3 @@ /** | ||
/** @param {VDirective} node */ | ||
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"( | ||
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=true][key.name.name='is']"( | ||
node | ||
@@ -101,0 +101,0 @@ ) { |
@@ -27,3 +27,4 @@ /** | ||
'v-model-argument': require('./syntaxes/v-model-argument'), | ||
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers') | ||
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'), | ||
'v-is': require('./syntaxes/v-is') | ||
} | ||
@@ -97,3 +98,4 @@ | ||
forbiddenVModelCustomModifiers: | ||
'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".' | ||
'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".', | ||
forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".' | ||
} | ||
@@ -100,0 +102,0 @@ }, |
@@ -70,3 +70,3 @@ /** | ||
/** @param {VDirective} node */ | ||
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"( | ||
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=true][key.name.name='is']"( | ||
node | ||
@@ -73,0 +73,0 @@ ) { |
@@ -224,3 +224,3 @@ /** | ||
/** | ||
* @param {Identifier | MemberExpression | ThisExpression} node | ||
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node | ||
* @param {RuleContext} context | ||
@@ -308,2 +308,10 @@ * @returns {UsedProps} | ||
} | ||
} else if (parent.type === 'ChainExpression') { | ||
const { usedNames, unknown, calls } = extractPatternOrThisProperties( | ||
parent, | ||
context | ||
) | ||
result.usedNames.addAll(usedNames) | ||
result.unknown = result.unknown || unknown | ||
result.calls.push(...calls) | ||
} | ||
@@ -580,3 +588,3 @@ return result | ||
const container = getVueComponentPropertiesContainer(node) | ||
const watcherNames = new Set() | ||
const watcherUsedProperties = new Set() | ||
for (const watcher of utils.iterateProperties( | ||
@@ -586,10 +594,38 @@ node, | ||
)) { | ||
// Process `watch: { foo /* <- this */ () {} }` | ||
let path | ||
for (const seg of watcher.name.split('.')) { | ||
path = path ? `${path}.${seg}` : seg | ||
watcherNames.add(path) | ||
watcherUsedProperties.add(path) | ||
} | ||
// Process `watch: { x: 'foo' /* <- this */ }` | ||
if (watcher.type === 'object') { | ||
const property = watcher.property | ||
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 | ||
} | ||
} else { | ||
handlerValueNode = property.value | ||
} | ||
if ( | ||
handlerValueNode && | ||
(handlerValueNode.type === 'Literal' || | ||
handlerValueNode.type === 'TemplateLiteral') | ||
) { | ||
const name = utils.getStringLiteralValue(handlerValueNode) | ||
if (name != null) { | ||
watcherUsedProperties.add(name) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
for (const prop of utils.iterateProperties(node, groups)) { | ||
if (watcherNames.has(prop.name)) { | ||
if (watcherUsedProperties.has(prop.name)) { | ||
continue | ||
@@ -596,0 +632,0 @@ } |
/** | ||
* @author Yosuke Ota | ||
* | ||
* issue https://github.com/vuejs/eslint-plugin-vue/issues/403 | ||
* Style guide: https://vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential | ||
* | ||
* I implemented it with reference to `no-confusing-v-for-v-if` | ||
* Style guide: https://v3.vuejs.org/style-guide/#avoid-v-if-with-v-for-essential | ||
*/ | ||
@@ -9,0 +6,0 @@ 'use strict' |
@@ -145,3 +145,3 @@ /** | ||
return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))] | ||
return fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1')) | ||
} | ||
@@ -148,0 +148,0 @@ }) |
@@ -114,6 +114,6 @@ /** | ||
messageId: 'unexpected', | ||
fix(fixer) { | ||
*fix(fixer) { | ||
if (hasComment || hasEscape) { | ||
// cannot fix | ||
return null | ||
return | ||
} | ||
@@ -130,2 +130,4 @@ const text = sourceCode.getText(value) | ||
yield fixer.removeRange(keyDirectiveRange) | ||
let attrValue | ||
@@ -141,6 +143,3 @@ if (quoteChar === '"') { | ||
} | ||
return [ | ||
fixer.removeRange(keyDirectiveRange), | ||
fixer.replaceText(expression, attrValue) | ||
] | ||
yield fixer.replaceText(expression, attrValue) | ||
} | ||
@@ -147,0 +146,0 @@ }) |
@@ -10,3 +10,4 @@ /** | ||
/** | ||
* @param {CallExpression} node | ||
* @param {CallExpression | ChainExpression} node | ||
* @returns {boolean} | ||
*/ | ||
@@ -36,2 +37,5 @@ function isMaybeUsedStopHandle(node) { | ||
} | ||
if (parent.type === 'ChainExpression') { | ||
return isMaybeUsedStopHandle(parent) | ||
} | ||
} | ||
@@ -38,0 +42,0 @@ return false |
@@ -188,3 +188,5 @@ /** | ||
node.type !== 'SpreadElement' && | ||
node.type !== 'TemplateLiteral' | ||
node.type !== 'TemplateLiteral' && | ||
// es2020 | ||
node.type !== 'ChainExpression' | ||
) { | ||
@@ -293,3 +295,3 @@ // Can not be sure that a node has no side effects | ||
}, | ||
fix(fixer) { | ||
*fix(fixer) { | ||
const propertyNode = property.node | ||
@@ -307,3 +309,3 @@ const firstUnorderedPropertyNode = firstUnorderedProperty.node | ||
if (hasSideEffectsPossibility) { | ||
return null | ||
return | ||
} | ||
@@ -319,2 +321,7 @@ const afterComma = sourceCode.getTokenAfter(propertyNode) | ||
const removeStart = hasAfterComma | ||
? codeStart | ||
: beforeComma.range[0] | ||
yield fixer.removeRange([removeStart, codeEnd]) | ||
const propertyCode = | ||
@@ -327,10 +334,3 @@ sourceCode.text.slice(codeStart, codeEnd) + | ||
const removeStart = hasAfterComma | ||
? codeStart | ||
: beforeComma.range[0] | ||
return [ | ||
fixer.removeRange([removeStart, codeEnd]), | ||
fixer.insertTextAfter(insertTarget, propertyCode) | ||
] | ||
yield fixer.insertTextAfter(insertTarget, propertyCode) | ||
} | ||
@@ -337,0 +337,0 @@ }) |
@@ -51,4 +51,4 @@ /** | ||
messageId: 'never', | ||
fix(fixer) { | ||
return paddingLines.map(([prevToken, nextToken]) => { | ||
*fix(fixer) { | ||
for (const [prevToken, nextToken] of paddingLines) { | ||
const start = prevToken.range[1] | ||
@@ -58,4 +58,4 @@ const end = nextToken.range[0] | ||
const lastSpaces = splitLines(paddingText).pop() | ||
return fixer.replaceTextRange([start, end], `\n${lastSpaces}`) | ||
}) | ||
yield fixer.replaceTextRange([start, end], `\n${lastSpaces}`) | ||
} | ||
} | ||
@@ -62,0 +62,0 @@ }) |
@@ -85,8 +85,14 @@ /** | ||
if (prop.value.type !== 'ObjectExpression') { | ||
return ( | ||
(prop.value.type !== 'CallExpression' && | ||
prop.value.type !== 'Identifier') || | ||
(prop.value.type === 'Identifier' && | ||
NATIVE_TYPES.has(prop.value.name)) | ||
) | ||
if (prop.value.type === 'Identifier') { | ||
return NATIVE_TYPES.has(prop.value.name) | ||
} | ||
if ( | ||
prop.value.type === 'CallExpression' || | ||
prop.value.type === 'MemberExpression' | ||
) { | ||
// OK | ||
return false | ||
} | ||
// NG | ||
return true | ||
} | ||
@@ -103,3 +109,3 @@ | ||
* Detects whether given value node is a Boolean type | ||
* @param {Expression | Pattern} value | ||
* @param {Expression} value | ||
* @return {boolean} | ||
@@ -128,3 +134,3 @@ */ | ||
function isBooleanProp(prop) { | ||
const value = utils.unwrapTypes(prop.value) | ||
const value = utils.skipTSAsExpression(prop.value) | ||
@@ -131,0 +137,0 @@ return ( |
@@ -88,3 +88,3 @@ /** | ||
description: 'require `emits` option with name triggered by `$emit()`', | ||
categories: undefined, | ||
categories: ['vue3-strongly-recommended'], | ||
url: 'https://eslint.vuejs.org/rules/require-explicit-emits.html' | ||
@@ -265,3 +265,3 @@ }, | ||
'CallExpression[arguments.0.type=Literal]'(node, { node: vueNode }) { | ||
const callee = node.callee | ||
const callee = utils.skipChainExpression(node.callee) | ||
const nameLiteralNode = node.arguments[0] | ||
@@ -292,10 +292,11 @@ if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') { | ||
verify(emitsDeclarations, nameLiteralNode, vueNode) | ||
} else if ( | ||
emit && | ||
emit.name === 'emit' && | ||
emit.member.object.type === 'Identifier' && | ||
contextReferenceIds.has(emit.member.object) | ||
) { | ||
// verify setup(props,context) {context.emit()} | ||
verify(emitsDeclarations, nameLiteralNode, vueNode) | ||
} else if (emit && emit.name === 'emit') { | ||
const memObject = utils.skipChainExpression(emit.member.object) | ||
if ( | ||
memObject.type === 'Identifier' && | ||
contextReferenceIds.has(memObject) | ||
) { | ||
// verify setup(props,context) {context.emit()} | ||
verify(emitsDeclarations, nameLiteralNode, vueNode) | ||
} | ||
} | ||
@@ -306,3 +307,4 @@ } | ||
if (emit && emit.name === '$emit') { | ||
if (utils.isThis(emit.member.object, context)) { | ||
const memObject = utils.skipChainExpression(emit.member.object) | ||
if (utils.isThis(memObject, context)) { | ||
// verify this.$emit() | ||
@@ -309,0 +311,0 @@ verify(emitsDeclarations, nameLiteralNode, vueNode) |
@@ -36,3 +36,3 @@ /** | ||
* Verify the given node | ||
* @param {MemberExpression | Identifier} node The node to verify | ||
* @param {MemberExpression | Identifier | ChainExpression} node The node to verify | ||
* @param {Expression} reportNode The node to report | ||
@@ -62,2 +62,8 @@ */ | ||
if (parent.type === 'ChainExpression') { | ||
// (this.$slots?.foo).x | ||
verify(parent, reportNode) | ||
return | ||
} | ||
if ( | ||
@@ -102,10 +108,7 @@ // this.$slots.foo.xxx | ||
MemberExpression(node) { | ||
const object = node.object | ||
const object = utils.skipChainExpression(node.object) | ||
if (object.type !== 'MemberExpression') { | ||
return | ||
} | ||
if ( | ||
object.property.type !== 'Identifier' || | ||
object.property.name !== '$slots' | ||
) { | ||
if (utils.getStaticPropertyName(object) !== '$slots') { | ||
return | ||
@@ -112,0 +115,0 @@ } |
@@ -51,3 +51,3 @@ /** | ||
/** | ||
* @param {Expression | Pattern} node | ||
* @param {Expression} node | ||
* @returns {string[]} | ||
@@ -126,6 +126,7 @@ */ | ||
/** | ||
* @param {Expression | Pattern} node | ||
* @param {Expression} targetNode | ||
* @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null } | ||
*/ | ||
function getValueType(node) { | ||
function getValueType(targetNode) { | ||
const node = utils.skipChainExpression(targetNode) | ||
if (node.type === 'CallExpression') { | ||
@@ -132,0 +133,0 @@ // Symbol(), Number() ... |
@@ -59,5 +59,5 @@ /** | ||
* @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot` | ||
* @returns {Fix[]} fix data | ||
* @returns {IterableIterator<Fix>} fix data | ||
*/ | ||
function fixSlotToVSlot(fixer, slotAttr, slotName, vBind) { | ||
function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) { | ||
const element = slotAttr.parent | ||
@@ -82,7 +82,6 @@ const scopeAttr = element.attributes.find( | ||
const replaceText = `v-slot${nameArgument}${scopeValue}` | ||
const fixers = [fixer.replaceText(slotAttr || scopeAttr, replaceText)] | ||
yield fixer.replaceText(slotAttr || scopeAttr, replaceText) | ||
if (slotAttr && scopeAttr) { | ||
fixers.push(fixer.remove(scopeAttr)) | ||
yield fixer.remove(scopeAttr) | ||
} | ||
return fixers | ||
} | ||
@@ -99,8 +98,8 @@ /** | ||
// fix to use `v-slot` | ||
fix(fixer) { | ||
*fix(fixer) { | ||
if (!canConvertFromSlotToVSlot(slotAttr)) { | ||
return null | ||
return | ||
} | ||
const slotName = slotAttr.value && slotAttr.value.value | ||
return fixSlotToVSlot(fixer, slotAttr, slotName, false) | ||
yield* fixSlotToVSlot(fixer, slotAttr, slotName, false) | ||
} | ||
@@ -119,5 +118,5 @@ }) | ||
// fix to use `v-slot` | ||
fix(fixer) { | ||
*fix(fixer) { | ||
if (!canConvertFromVBindSlotToVSlot(slotAttr)) { | ||
return null | ||
return | ||
} | ||
@@ -128,3 +127,3 @@ const slotName = | ||
sourceCode.getText(slotAttr.value.expression).trim() | ||
return fixSlotToVSlot(fixer, slotAttr, slotName, true) | ||
yield* fixSlotToVSlot(fixer, slotAttr, slotName, true) | ||
} | ||
@@ -131,0 +130,0 @@ }) |
@@ -77,12 +77,13 @@ /** | ||
messageId: 'forbiddenSlotScopeAttribute', | ||
fix: fixToUpgrade | ||
? // fix to use `v-slot` | ||
(fixer) => { | ||
const startTag = scopeAttr.parent | ||
if (!canConvertToVSlot(startTag)) { | ||
return null | ||
} | ||
return fixSlotScopeToVSlot(fixer, scopeAttr) | ||
} | ||
: null | ||
fix(fixer) { | ||
if (!fixToUpgrade) { | ||
return null | ||
} | ||
// fix to use `v-slot` | ||
const startTag = scopeAttr.parent | ||
if (!canConvertToVSlot(startTag)) { | ||
return null | ||
} | ||
return fixSlotScopeToVSlot(fixer, scopeAttr) | ||
} | ||
}) | ||
@@ -89,0 +90,0 @@ } |
@@ -72,3 +72,3 @@ /** | ||
// fix to use `slot` (downgrade) | ||
fix: (fixer) => { | ||
fix(fixer) { | ||
if (!canConvertToSlot(vSlotAttr)) { | ||
@@ -75,0 +75,0 @@ return null |
@@ -88,2 +88,6 @@ /** | ||
} | ||
if (expression.optional) { | ||
// Allow optional chaining | ||
return null | ||
} | ||
const callee = expression.callee | ||
@@ -90,0 +94,0 @@ if (callee.type !== 'Identifier') { |
@@ -35,4 +35,19 @@ /** | ||
/** | ||
* Check whether the given node can be LHS. | ||
* Check whether the given node is a MemberExpression containing an optional chaining. | ||
* e.g. | ||
* - `a?.b` | ||
* - `a?.b.c` | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining. | ||
*/ | ||
function isOptionalChainingMemberExpression(node) { | ||
return ( | ||
node.type === 'ChainExpression' && | ||
node.expression.type === 'MemberExpression' | ||
) | ||
} | ||
/** | ||
* Check whether the given node can be LHS (left-hand side). | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node can be LHS. | ||
@@ -44,2 +59,29 @@ */ | ||
/** | ||
* Check whether the given node is a MemberExpression of a possibly null object. | ||
* e.g. | ||
* - `(a?.b).c` | ||
* - `(null).foo` | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node is a MemberExpression of a possibly null object. | ||
*/ | ||
function maybeNullObjectMemberExpression(node) { | ||
if (node.type !== 'MemberExpression') { | ||
return false | ||
} | ||
const { object } = node | ||
if (object.type === 'ChainExpression') { | ||
// `(a?.b).c` | ||
return true | ||
} | ||
if (object.type === 'Literal' && object.value === null && !object.bigint) { | ||
// `(null).foo` | ||
return true | ||
} | ||
if (object.type === 'MemberExpression') { | ||
return maybeNullObjectMemberExpression(object) | ||
} | ||
return false | ||
} | ||
// ------------------------------------------------------------------------------ | ||
@@ -62,4 +104,8 @@ // Rule Definition | ||
"'.sync' modifiers aren't supported on <{{name}}> non Vue-components.", | ||
unexpectedOptionalChaining: | ||
"Optional chaining cannot appear in 'v-bind' with '.sync' modifiers.", | ||
unexpectedNonLhsExpression: | ||
"'.sync' modifiers require the attribute value which is valid as LHS.", | ||
unexpectedNullObject: | ||
"'.sync' modifier has potential null object property access.", | ||
unexpectedUpdateIterationVariable: | ||
@@ -89,12 +135,29 @@ "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself." | ||
if (!node.value || !node.value.expression) { | ||
if (!node.value) { | ||
return | ||
} | ||
if (!isLhs(node.value.expression)) { | ||
const expression = node.value.expression | ||
if (!expression) { | ||
// Parsing error | ||
return | ||
} | ||
if (isOptionalChainingMemberExpression(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: 'unexpectedOptionalChaining' | ||
}) | ||
} else if (!isLhs(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: 'unexpectedNonLhsExpression' | ||
}) | ||
} else if (maybeNullObjectMemberExpression(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: 'unexpectedNullObject' | ||
}) | ||
} | ||
@@ -101,0 +164,0 @@ |
@@ -40,4 +40,19 @@ /** | ||
/** | ||
* Check whether the given node can be LHS. | ||
* Check whether the given node is a MemberExpression containing an optional chaining. | ||
* e.g. | ||
* - `a?.b` | ||
* - `a?.b.c` | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining. | ||
*/ | ||
function isOptionalChainingMemberExpression(node) { | ||
return ( | ||
node.type === 'ChainExpression' && | ||
node.expression.type === 'MemberExpression' | ||
) | ||
} | ||
/** | ||
* Check whether the given node can be LHS (left-hand side). | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node can be LHS. | ||
@@ -50,2 +65,29 @@ */ | ||
/** | ||
* Check whether the given node is a MemberExpression of a possibly null object. | ||
* e.g. | ||
* - `(a?.b).c` | ||
* - `(null).foo` | ||
* @param {ASTNode} node The node to check. | ||
* @returns {boolean} `true` if the node is a MemberExpression of a possibly null object. | ||
*/ | ||
function maybeNullObjectMemberExpression(node) { | ||
if (node.type !== 'MemberExpression') { | ||
return false | ||
} | ||
const { object } = node | ||
if (object.type === 'ChainExpression') { | ||
// `(a?.b).c` | ||
return true | ||
} | ||
if (object.type === 'Literal' && object.value === null && !object.bigint) { | ||
// `(null).foo` | ||
return true | ||
} | ||
if (object.type === 'MemberExpression') { | ||
return maybeNullObjectMemberExpression(object) | ||
} | ||
return false | ||
} | ||
/** | ||
* Get the variable by names. | ||
@@ -81,2 +123,3 @@ * @param {string} name The variable name to find. | ||
/** @type {RuleModule} */ | ||
module.exports = { | ||
@@ -91,3 +134,21 @@ meta: { | ||
fixable: null, | ||
schema: [] | ||
schema: [], | ||
messages: { | ||
unexpectedInvalidElement: | ||
"'v-model' directives aren't supported on <{{name}}> elements.", | ||
unexpectedInputFile: | ||
"'v-model' directives don't support 'file' input type.", | ||
unexpectedArgument: "'v-model' directives require no argument.", | ||
unexpectedModifier: | ||
"'v-model' directives don't support the modifier '{{name}}'.", | ||
missingValue: "'v-model' directives require that attribute value.", | ||
unexpectedOptionalChaining: | ||
"Optional chaining cannot appear in 'v-model' directives.", | ||
unexpectedNonLhsExpression: | ||
"'v-model' directives require the attribute value which is valid as LHS.", | ||
unexpectedNullObject: | ||
"'v-model' directive has potential null object property access.", | ||
unexpectedUpdateIterationVariable: | ||
"'v-model' directives cannot update the iteration variable '{{varName}}' itself." | ||
} | ||
}, | ||
@@ -106,4 +167,3 @@ /** @param {RuleContext} context */ | ||
loc: node.loc, | ||
message: | ||
"'v-model' directives aren't supported on <{{name}}> elements.", | ||
messageId: 'unexpectedInvalidElement', | ||
data: { name } | ||
@@ -117,3 +177,3 @@ }) | ||
loc: node.loc, | ||
message: "'v-model' directives don't support 'file' input type." | ||
messageId: 'unexpectedInputFile' | ||
}) | ||
@@ -127,3 +187,3 @@ } | ||
loc: node.loc, | ||
message: "'v-model' directives require no argument." | ||
messageId: 'unexpectedArgument' | ||
}) | ||
@@ -137,4 +197,3 @@ } | ||
loc: node.loc, | ||
message: | ||
"'v-model' directives don't support the modifier '{{name}}'.", | ||
messageId: 'unexpectedModifier', | ||
data: { name: modifier.name } | ||
@@ -150,17 +209,29 @@ }) | ||
loc: node.loc, | ||
message: "'v-model' directives require that attribute value." | ||
messageId: 'missingValue' | ||
}) | ||
return | ||
} | ||
if (!node.value.expression) { | ||
const expression = node.value.expression | ||
if (!expression) { | ||
// Parsing error | ||
return | ||
} | ||
if (!isLhs(node.value.expression)) { | ||
if (isOptionalChainingMemberExpression(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
message: | ||
"'v-model' directives require the attribute value which is valid as LHS." | ||
messageId: 'unexpectedOptionalChaining' | ||
}) | ||
} else if (!isLhs(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: 'unexpectedNonLhsExpression' | ||
}) | ||
} else if (maybeNullObjectMemberExpression(expression)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
messageId: 'unexpectedNullObject' | ||
}) | ||
} | ||
@@ -179,4 +250,4 @@ | ||
loc: node.loc, | ||
message: | ||
"'v-model' directives cannot update the iteration variable '{{varName}}' itself.", | ||
messageId: 'unexpectedUpdateIterationVariable', | ||
data: { varName: id.name } | ||
@@ -183,0 +254,0 @@ }) |
@@ -86,3 +86,3 @@ /** | ||
* @typedef { { type: 'array', name: string, groupName: GroupName, node: Literal | TemplateLiteral } } ComponentArrayPropertyData | ||
* @typedef { { type: 'object', name: string, groupName: GroupName, node: Identifier | Literal | TemplateLiteral } } ComponentObjectPropertyData | ||
* @typedef { { type: 'object', name: string, groupName: GroupName, node: Identifier | Literal | TemplateLiteral, property: Property } } ComponentObjectPropertyData | ||
* @typedef { ComponentArrayPropertyData | ComponentObjectPropertyData } ComponentPropertyData | ||
@@ -319,3 +319,2 @@ */ | ||
}, | ||
/** | ||
@@ -329,14 +328,2 @@ * Checks whether the given value is defined. | ||
/** | ||
* Check whether the given node is the root element or not. | ||
* @param {VElement} node The element node to check. | ||
* @returns {boolean} `true` if the node is the root element. | ||
*/ | ||
isRootElement(node) { | ||
return ( | ||
node.parent.type === 'VDocumentFragment' || | ||
node.parent.parent.type === 'VDocumentFragment' | ||
) | ||
}, | ||
/** | ||
* Get the previous sibling element of the given element. | ||
@@ -597,3 +584,4 @@ * @param {VElement} node The element node to get the previous sibling element. | ||
this.hasAttribute(node, 'is') || | ||
this.hasDirective(node, 'bind', 'is') | ||
this.hasDirective(node, 'bind', 'is') || | ||
this.hasDirective(node, 'is') | ||
) | ||
@@ -655,33 +643,3 @@ }, | ||
}, | ||
/** | ||
* Parse member expression node to get array with all of its parts | ||
* @param {ESNode} node MemberExpression | ||
* @returns {string[]} | ||
*/ | ||
parseMemberExpression(node) { | ||
const members = [] | ||
if (node.type === 'MemberExpression') { | ||
/** @type {Expression | Super} */ | ||
let memberExpression = node | ||
while (memberExpression.type === 'MemberExpression') { | ||
if (memberExpression.property.type === 'Identifier') { | ||
members.push(memberExpression.property.name) | ||
} | ||
memberExpression = memberExpression.object | ||
} | ||
if (memberExpression.type === 'ThisExpression') { | ||
members.push('this') | ||
} else if (memberExpression.type === 'Identifier') { | ||
members.push(memberExpression.name) | ||
} | ||
} | ||
return members.reverse() | ||
}, | ||
/** | ||
* Gets the property name of a given node. | ||
@@ -732,3 +690,3 @@ * @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get. | ||
propName, | ||
value: unwrapTypes(prop.value), | ||
value: skipTSAsExpression(prop.value), | ||
node: prop | ||
@@ -741,3 +699,3 @@ } | ||
propName: null, | ||
value: unwrapTypes(prop.value), | ||
value: skipTSAsExpression(prop.value), | ||
node: prop | ||
@@ -805,3 +763,3 @@ } | ||
emitName, | ||
value: unwrapTypes(prop.value), | ||
value: skipTSAsExpression(prop.value), | ||
node: prop | ||
@@ -814,3 +772,3 @@ } | ||
emitName: null, | ||
value: unwrapTypes(prop.value), | ||
value: skipTSAsExpression(prop.value), | ||
node: prop | ||
@@ -874,3 +832,3 @@ } | ||
/** @type {Expression} */ | ||
const propValue = unwrapTypes(cp.value) | ||
const propValue = skipTSAsExpression(cp.value) | ||
/** @type {BlockStatement | null} */ | ||
@@ -1069,3 +1027,3 @@ let value = null | ||
if (callee.type === 'MemberExpression') { | ||
const calleeObject = unwrapTypes(callee.object) | ||
const calleeObject = skipTSAsExpression(callee.object) | ||
@@ -1139,3 +1097,3 @@ if ( | ||
/** @type {Set<Property> | undefined} */ | ||
let usedGetter = new Set() | ||
let usedGetter | ||
for (const item of node.properties) { | ||
@@ -1175,3 +1133,9 @@ if (item.type === 'Property') { | ||
} | ||
yield { type: 'object', name, groupName, node: key } | ||
yield { | ||
type: 'object', | ||
name, | ||
groupName, | ||
node: key, | ||
property: item | ||
} | ||
} | ||
@@ -1329,7 +1293,7 @@ } | ||
const nodes = [] | ||
let n = node | ||
let n = skipChainExpression(node) | ||
while (n.type === 'MemberExpression') { | ||
nodes.push(n) | ||
n = n.object | ||
n = skipChainExpression(n.object) | ||
} | ||
@@ -1340,39 +1304,2 @@ | ||
/** | ||
* Parse CallExpression or MemberExpression to get simplified version without arguments | ||
* | ||
* @param {ESNode} node The node to parse (MemberExpression | CallExpression) | ||
* @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()' | ||
*/ | ||
parseMemberOrCallExpression(node) { | ||
const parsedCallee = [] | ||
let n = node | ||
let isFunc | ||
while (n.type === 'MemberExpression' || n.type === 'CallExpression') { | ||
if (n.type === 'CallExpression') { | ||
n = n.callee | ||
isFunc = true | ||
} else { | ||
if (n.computed) { | ||
parsedCallee.push(`[]${isFunc ? '()' : ''}`) | ||
} else if (n.property.type === 'Identifier') { | ||
parsedCallee.push(n.property.name + (isFunc ? '()' : '')) | ||
} | ||
isFunc = false | ||
n = n.object | ||
} | ||
} | ||
if (n.type === 'Identifier') { | ||
parsedCallee.push(n.name) | ||
} | ||
if (n.type === 'ThisExpression') { | ||
parsedCallee.push('this') | ||
} | ||
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[') | ||
}, | ||
/** | ||
* return two string editdistance | ||
@@ -1436,14 +1363,13 @@ * @param {string} a string a to compare | ||
/** | ||
* Unwrap typescript types like "X as F" | ||
* @template T | ||
* @param {T} node | ||
* @return {T} | ||
* Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it. | ||
*/ | ||
unwrapTypes, | ||
skipTSAsExpression, | ||
/** | ||
* Unwrap AssignmentPattern like "(a = 1) => ret" | ||
* @param { AssignmentPattern | RestElement | ArrayPattern | ObjectPattern | Identifier } node | ||
* @return { RestElement | ArrayPattern | ObjectPattern | Identifier} | ||
* Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it. | ||
*/ | ||
unwrapAssignmentPattern, | ||
skipDefaultParamValue, | ||
/** | ||
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. | ||
*/ | ||
skipChainExpression, | ||
@@ -1498,2 +1424,3 @@ /** | ||
const pathNodes = [] | ||
/** @type {MemberExpression | Identifier | ChainExpression} */ | ||
let node = props | ||
@@ -1519,6 +1446,5 @@ let target = node.parent | ||
} else if (target.type === 'CallExpression') { | ||
if (node !== props && target.callee === node) { | ||
const callName = getStaticPropertyName( | ||
/** @type {MemberExpression} */ (node) | ||
) | ||
if (pathNodes.length > 0 && target.callee === node) { | ||
const mem = pathNodes[pathNodes.length - 1] | ||
const callName = getStaticPropertyName(mem) | ||
if ( | ||
@@ -1546,2 +1472,6 @@ callName && | ||
} | ||
} else if (target.type === 'ChainExpression') { | ||
node = target | ||
target = target.parent | ||
continue // loop | ||
} | ||
@@ -1682,5 +1612,3 @@ | ||
(prop) => | ||
prop.type === 'Property' && | ||
getStaticPropertyName(prop) === name && | ||
filter(prop) | ||
isProperty(prop) && getStaticPropertyName(prop) === name && filter(prop) | ||
: /** | ||
@@ -1690,3 +1618,3 @@ * @param {Property | SpreadElement} prop | ||
*/ | ||
(prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name | ||
(prop) => isProperty(prop) && getStaticPropertyName(prop) === name | ||
return node.properties.find(predicate) || null | ||
@@ -1709,3 +1637,3 @@ } | ||
(prop) => | ||
prop.type === 'Property' && | ||
isAssignmentProperty(prop) && | ||
getStaticPropertyName(prop) === name && | ||
@@ -1717,3 +1645,4 @@ filter(prop) | ||
*/ | ||
(prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name | ||
(prop) => | ||
isAssignmentProperty(prop) && getStaticPropertyName(prop) === name | ||
return node.properties.find(predicate) || null | ||
@@ -1748,8 +1677,8 @@ } | ||
/** | ||
* Unwrap typescript types like "X as F" | ||
* @template T | ||
* @param {T} node | ||
* @return {T} | ||
* Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it. | ||
* @template T Node type | ||
* @param {T | TSAsExpression} node The node to address. | ||
* @returns {T} The `TSAsExpression#expression` value if the node is a `TSAsExpression` node. Otherwise, the node. | ||
*/ | ||
function unwrapTypes(node) { | ||
function skipTSAsExpression(node) { | ||
if (!node) { | ||
@@ -1761,4 +1690,5 @@ return node | ||
// @ts-expect-error | ||
return unwrapTypes(node.expression) | ||
return skipTSAsExpression(node.expression) | ||
} | ||
// @ts-expect-error | ||
return node | ||
@@ -1794,14 +1724,17 @@ } | ||
/** | ||
* Unwrap AssignmentPattern like "(a = 1) => ret" | ||
* @param { AssignmentPattern | RestElement | ArrayPattern | ObjectPattern | Identifier } node | ||
* @return { RestElement | ArrayPattern | ObjectPattern | Identifier} | ||
* Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it. | ||
* @template T Node type | ||
* @param {T | AssignmentPattern} node The node to address. | ||
* @return {T} The `AssignmentPattern#left` value if the node is a `AssignmentPattern` node. Otherwise, the node. | ||
*/ | ||
function unwrapAssignmentPattern(node) { | ||
function skipDefaultParamValue(node) { | ||
if (!node) { | ||
return node | ||
} | ||
// @ts-expect-error | ||
if (node.type === 'AssignmentPattern') { | ||
// @ts-expect-error | ||
return unwrapAssignmentPattern(node.left) | ||
return skipDefaultParamValue(node.left) | ||
} | ||
// @ts-expect-error | ||
return node | ||
@@ -1811,2 +1744,21 @@ } | ||
/** | ||
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. | ||
* @template T Node type | ||
* @param {T | ChainExpression} node The node to address. | ||
* @returns {T} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node. | ||
*/ | ||
function skipChainExpression(node) { | ||
if (!node) { | ||
return node | ||
} | ||
// @ts-expect-error | ||
if (node.type === 'ChainExpression') { | ||
// @ts-expect-error | ||
return skipChainExpression(node.expression) | ||
} | ||
// @ts-expect-error | ||
return node | ||
} | ||
/** | ||
* Gets the property name of a given node. | ||
@@ -1909,3 +1861,3 @@ * @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get. | ||
if (callee.type === 'MemberExpression') { | ||
const calleeObject = unwrapTypes(callee.object) | ||
const calleeObject = skipTSAsExpression(callee.object) | ||
@@ -1964,3 +1916,4 @@ if (calleeObject.type === 'Identifier') { | ||
node.arguments.length > 0 && | ||
unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression' | ||
skipTSAsExpression(node.arguments.slice(-1)[0]).type === | ||
'ObjectExpression' | ||
) | ||
@@ -1983,3 +1936,3 @@ } | ||
node.arguments.length && | ||
unwrapTypes(node.arguments[0]).type === 'ObjectExpression' | ||
skipTSAsExpression(node.arguments[0]).type === 'ObjectExpression' | ||
) | ||
@@ -2004,3 +1957,3 @@ } | ||
isVueComponentFile(parent, filePath) && | ||
unwrapTypes(parent.declaration) === node | ||
skipTSAsExpression(parent.declaration) === node | ||
) { | ||
@@ -2013,3 +1966,3 @@ return 'export' | ||
isVueComponent(parent) && | ||
unwrapTypes(parent.arguments.slice(-1)[0]) === node | ||
skipTSAsExpression(parent.arguments.slice(-1)[0]) === node | ||
) { | ||
@@ -2020,3 +1973,6 @@ return 'definition' | ||
// new Vue({}) | ||
if (isVueInstance(parent) && unwrapTypes(parent.arguments[0]) === node) { | ||
if ( | ||
isVueInstance(parent) && | ||
skipTSAsExpression(parent.arguments[0]) === node | ||
) { | ||
return 'instance' | ||
@@ -2023,0 +1979,0 @@ } |
{ | ||
"name": "eslint-plugin-vue", | ||
"version": "7.0.0-alpha.10", | ||
"version": "7.0.0-beta.0", | ||
"description": "Official ESLint plugin for Vue.js", | ||
@@ -53,6 +53,6 @@ "main": "lib/index.js", | ||
"peerDependencies": { | ||
"eslint": "^6.0.0 || ^7.0.0" | ||
"eslint": "^6.2.0 || ^7.0.0" | ||
}, | ||
"dependencies": { | ||
"eslint-utils": "^2.0.0", | ||
"eslint-utils": "^2.1.0", | ||
"natural-compare": "^1.4.0", | ||
@@ -77,3 +77,2 @@ "semver": "^7.3.2", | ||
"eslint-plugin-vue": "file:.", | ||
"eslint-plugin-vue-libs": "^4.0.0", | ||
"eslint4b": "^7.0.0", | ||
@@ -80,0 +79,0 @@ "lodash": "^4.17.15", |
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
729494
22
196
22430
Updatedeslint-utils@^2.1.0