diff --git a/.circleci/config.yml b/.circleci/config.yml
index d20690e4c..d593e239e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -43,7 +43,7 @@ jobs:
- run:
name: Install eslint@6
command: |
- npm install -D eslint@6.0.0
+ npm install -D eslint@6.2.0
- run:
name: Install dependencies
command: npm install
diff --git a/.eslintrc.js b/.eslintrc.js
index f5eb3a23a..704f09732 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -135,7 +135,9 @@ module.exports = {
{
pattern: `https://eslint.vuejs.org/rules/{{name}}.html`
}
- ]
+ ],
+
+ 'eslint-plugin/fixer-return': 'off'
}
}
]
diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue
index a4d6232a1..391ae5490 100644
--- a/docs/.vuepress/components/eslint-code-block.vue
+++ b/docs/.vuepress/components/eslint-code-block.vue
@@ -89,7 +89,7 @@ export default {
rules: this.rules,
parser: 'vue-eslint-parser',
parserOptions: {
- ecmaVersion: 2019,
+ ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
diff --git a/docs/rules/valid-v-bind-sync.md b/docs/rules/valid-v-bind-sync.md
index 9ebd51fd1..163d8b217 100644
--- a/docs/rules/valid-v-bind-sync.md
+++ b/docs/rules/valid-v-bind-sync.md
@@ -15,7 +15,8 @@ This rule checks whether every `.sync` modifier on `v-bind` directives is valid.
This rule reports `.sync` modifier on `v-bind` directives in the following cases:
-- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. ` `
+- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. ` `, ` `
+- The `.sync` modifier has potential null object property access. E.g. ` `
- The `.sync` modifier is on non Vue-components. E.g. ` `
- The `.sync` modifier's reference is iteration variables. E.g. `
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 4579d6c72..8de4d3e9d 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -18,7 +18,7 @@ yarn add -D eslint eslint-plugin-vue@next
```
::: tip Requirements
-- ESLint v6.0.0 and above
+- ESLint v6.2.0 and above
- Node.js v8.10.0 and above
:::
diff --git a/lib/configs/base.js b/lib/configs/base.js
index 71279b9a0..2521a623c 100644
--- a/lib/configs/base.js
+++ b/lib/configs/base.js
@@ -6,7 +6,7 @@
module.exports = {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
},
env: {
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
index a4771c0bf..dffca0c88 100644
--- a/lib/rules/component-name-in-template-casing.js
+++ b/lib/rules/component-name-in-template-casing.js
@@ -135,16 +135,13 @@ module.exports = {
name,
caseType
},
- 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}`)
- ]
}
})
}
diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js
index cd7f73443..6c5f072c3 100644
--- a/lib/rules/custom-event-name-casing.js
+++ b/lib/rules/custom-event-name-casing.js
@@ -48,7 +48,7 @@ function getNameParamNode(node) {
* @param {CallExpression} node CallExpression
*/
function getCalleeMemberNode(node) {
- const callee = node.callee
+ const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
@@ -116,7 +116,7 @@ module.exports = {
utils.compositingVisitors(
utils.defineVueVisitor(context, {
onSetupFunctionEnter(node, { node: vueNode }) {
- const contextParam = utils.unwrapAssignmentPattern(node.params[1])
+ const contextParam = utils.skipDefaultParamValue(node.params[1])
if (!contextParam) {
// no arguments
return
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index f8841f782..b54c20671 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -163,7 +163,7 @@ module.exports = {
elementType: ELEMENT_TYPE_MESSAGES[elementType],
name: node.rawName
},
- fix: (fixer) => {
+ fix(fixer) {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLTagClose') {
@@ -187,7 +187,7 @@ module.exports = {
elementType: ELEMENT_TYPE_MESSAGES[elementType],
name: node.rawName
},
- fix: (fixer) => {
+ fix(fixer) {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLSelfClosingTagClose') {
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index da11e0426..9812d61c0 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -26,16 +26,17 @@ const TIMED_FUNCTIONS = [
* @param {CallExpression} node
*/
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
)
}
@@ -44,18 +45,16 @@ function isTimedFunction(node) {
* @param {CallExpression} node
*/
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)
)
}
return false
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
index a09f36bf0..f393f59ef 100644
--- a/lib/rules/no-deprecated-events-api.js
+++ b/lib/rules/no-deprecated-events-api.js
@@ -32,13 +32,26 @@ module.exports = {
/** @param {RuleContext} context */
create(context) {
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) || ''
+ )
) {
return
}
diff --git a/lib/rules/no-deprecated-v-bind-sync.js b/lib/rules/no-deprecated-v-bind-sync.js
index 97dfe51bd..786ecb927 100644
--- a/lib/rules/no-deprecated-v-bind-sync.js
+++ b/lib/rules/no-deprecated-v-bind-sync.js
@@ -39,7 +39,7 @@ module.exports = {
node,
loc: node.loc,
messageId: 'syncModifierIsDeprecated',
- fix: (fixer) => {
+ fix(fixer) {
if (node.key.argument == null) {
// is using spread syntax
return null
diff --git a/lib/rules/no-deprecated-v-on-number-modifiers.js b/lib/rules/no-deprecated-v-on-number-modifiers.js
index 544c1ee9f..24341580a 100644
--- a/lib/rules/no-deprecated-v-on-number-modifiers.js
+++ b/lib/rules/no-deprecated-v-on-number-modifiers.js
@@ -47,7 +47,7 @@ module.exports = {
context.report({
node: modifier,
messageId: 'numberModifierIsDeprecated',
- fix: (fixer) => {
+ fix(fixer) {
const key = keyCodeToKey[keyCodes]
if (!key) return null
diff --git a/lib/rules/no-deprecated-vue-config-keycodes.js b/lib/rules/no-deprecated-vue-config-keycodes.js
index b30db3fed..4db268e44 100644
--- a/lib/rules/no-deprecated-vue-config-keycodes.js
+++ b/lib/rules/no-deprecated-vue-config-keycodes.js
@@ -4,6 +4,8 @@
*/
'use strict'
+const utils = require('../utils')
+
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -31,7 +33,7 @@ module.exports = {
"MemberExpression[property.type='Identifier'][property.name='keyCodes']"(
node
) {
- const config = node.object
+ const config = utils.skipChainExpression(node.object)
if (
config.type !== 'MemberExpression' ||
config.property.type !== 'Identifier' ||
diff --git a/lib/rules/no-multiple-slot-args.js b/lib/rules/no-multiple-slot-args.js
index f98e058ec..474a56ba2 100644
--- a/lib/rules/no-multiple-slot-args.js
+++ b/lib/rules/no-multiple-slot-args.js
@@ -100,15 +100,12 @@ module.exports = {
return utils.defineVueVisitor(context, {
/** @param {MemberExpression} node */
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
}
if (!utils.isThis(object.object, context)) {
diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js
index 3c6ce4446..e77f63c4d 100644
--- a/lib/rules/no-setup-props-destructure.js
+++ b/lib/rules/no-setup-props-destructure.js
@@ -49,18 +49,18 @@ module.exports = {
return
}
+ 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)
}
if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) {
report(left, 'getProperty')
@@ -84,7 +84,7 @@ module.exports = {
}
},
onSetupFunctionEnter(node) {
- const propsParam = utils.unwrapAssignmentPattern(node.params[0])
+ const propsParam = utils.skipDefaultParamValue(node.params[0])
if (!propsParam) {
// no arguments
return
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index 951d6a2db..f7f30dc84 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -221,7 +221,7 @@ function getObjectPatternPropertyPatternTracker(pattern) {
}
/**
- * @param {Identifier | MemberExpression | ThisExpression} node
+ * @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
* @param {RuleContext} context
* @returns {UsedProps}
*/
@@ -304,6 +304,14 @@ function extractPatternOrThisProperties(node, context) {
}
}
}
+ } 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)
}
return result
}
diff --git a/lib/rules/no-useless-mustaches.js b/lib/rules/no-useless-mustaches.js
index 89d8d5422..b8a2057d8 100644
--- a/lib/rules/no-useless-mustaches.js
+++ b/lib/rules/no-useless-mustaches.js
@@ -142,7 +142,7 @@ module.exports = {
return null
}
- return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))]
+ return fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))
}
})
}
diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js
index d64e95535..01549ca6e 100644
--- a/lib/rules/no-useless-v-bind.js
+++ b/lib/rules/no-useless-v-bind.js
@@ -111,10 +111,10 @@ module.exports = {
context.report({
node,
messageId: 'unexpected',
- fix(fixer) {
+ *fix(fixer) {
if (hasComment || hasEscape) {
// cannot fix
- return null
+ return
}
const text = sourceCode.getText(value)
const quoteChar = text[0]
@@ -126,6 +126,8 @@ module.exports = {
node.key.name.range[1] + (shorthand ? 0 : 1)
]
+ yield fixer.removeRange(keyDirectiveRange)
+
let attrValue
if (quoteChar === '"') {
attrValue = strValue.replace(DOUBLE_QUOTES_RE, '"')
@@ -136,10 +138,7 @@ module.exports = {
.replace(DOUBLE_QUOTES_RE, '"')
.replace(SINGLE_QUOTES_RE, ''')
}
- return [
- fixer.removeRange(keyDirectiveRange),
- fixer.replaceText(expression, attrValue)
- ]
+ yield fixer.replaceText(expression, attrValue)
}
})
}
diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js
index fc46dcd6f..5f037fb30 100644
--- a/lib/rules/no-watch-after-await.js
+++ b/lib/rules/no-watch-after-await.js
@@ -7,7 +7,8 @@ const { ReferenceTracker } = require('eslint-utils')
const utils = require('../utils')
/**
- * @param {CallExpression} node
+ * @param {CallExpression | ChainExpression} node
+ * @returns {boolean}
*/
function isMaybeUsedStopHandle(node) {
const parent = node.parent
@@ -32,6 +33,9 @@ function isMaybeUsedStopHandle(node) {
// [watch()]
return true
}
+ if (parent.type === 'ChainExpression') {
+ return isMaybeUsedStopHandle(parent)
+ }
}
return false
}
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index fcff89ab2..92b9b4c15 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -185,7 +185,9 @@ function isNotSideEffectsNode(node, visitorKeys) {
node.type !== 'ConditionalExpression' &&
// es2015
node.type !== 'SpreadElement' &&
- node.type !== 'TemplateLiteral'
+ node.type !== 'TemplateLiteral' &&
+ // es2020
+ node.type !== 'ChainExpression'
) {
// Can not be sure that a node has no side effects
result = false
@@ -289,7 +291,7 @@ module.exports = {
firstUnorderedPropertyName: firstUnorderedProperty.name,
line
},
- fix(fixer) {
+ *fix(fixer) {
const propertyNode = property.node
const firstUnorderedPropertyNode = firstUnorderedProperty.node
const hasSideEffectsPossibility = propertiesNodes
@@ -302,7 +304,7 @@ module.exports = {
!isNotSideEffectsNode(property, sourceCode.visitorKeys)
)
if (hasSideEffectsPossibility) {
- return null
+ return
}
const afterComma = sourceCode.getTokenAfter(propertyNode)
const hasAfterComma = isComma(afterComma)
@@ -313,6 +315,11 @@ module.exports = {
? afterComma.range[1]
: propertyNode.range[1]
+ const removeStart = hasAfterComma
+ ? codeStart
+ : beforeComma.range[0]
+ yield fixer.removeRange([removeStart, codeEnd])
+
const propertyCode =
sourceCode.text.slice(codeStart, codeEnd) +
(hasAfterComma ? '' : ',')
@@ -320,14 +327,7 @@ module.exports = {
firstUnorderedPropertyNode
)
- const removeStart = hasAfterComma
- ? codeStart
- : beforeComma.range[0]
-
- return [
- fixer.removeRange([removeStart, codeEnd]),
- fixer.insertTextAfter(insertTarget, propertyCode)
- ]
+ yield fixer.insertTextAfter(insertTarget, propertyCode)
}
})
}
diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js
index cc4773703..c1b652455 100644
--- a/lib/rules/padding-line-between-blocks.js
+++ b/lib/rules/padding-line-between-blocks.js
@@ -48,14 +48,14 @@ function verifyForNever(context, prevBlock, nextBlock, betweenTokens) {
context.report({
node: nextBlock,
messageId: 'never',
- fix(fixer) {
- return paddingLines.map(([prevToken, nextToken]) => {
+ *fix(fixer) {
+ for (const [prevToken, nextToken] of paddingLines) {
const start = prevToken.range[1]
const end = nextToken.range[0]
const paddingText = context.getSourceCode().text.slice(start, end)
const lastSpaces = splitLines(paddingText).pop()
- return fixer.replaceTextRange([start, end], `\n${lastSpaces}`)
- })
+ yield fixer.replaceTextRange([start, end], `\n${lastSpaces}`)
+ }
}
})
}
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index ec73e369b..e0e29b69e 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -82,12 +82,18 @@ module.exports = {
function findPropsWithoutDefaultValue(props) {
return props.filter((prop) => {
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
}
return (
@@ -99,7 +105,7 @@ module.exports = {
/**
* Detects whether given value node is a Boolean type
- * @param {Expression | Pattern} value
+ * @param {Expression} value
* @return {boolean}
*/
function isValueNodeOfBooleanType(value) {
@@ -123,7 +129,7 @@ module.exports = {
* @return {Boolean}
*/
function isBooleanProp(prop) {
- const value = utils.unwrapTypes(prop.value)
+ const value = utils.skipTSAsExpression(prop.value)
return (
isValueNodeOfBooleanType(value) ||
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index 7dc1706bb..27ae600fa 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -261,7 +261,7 @@ module.exports = {
* @param {VueObjectData} data
*/
'CallExpression[arguments.0.type=Literal]'(node, { node: vueNode }) {
- const callee = node.callee
+ const callee = utils.skipChainExpression(node.callee)
const nameLiteralNode = node.arguments[0]
if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
// cannot check
@@ -287,20 +287,22 @@ module.exports = {
if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
// verify setup(props,{emit}) {emit()}
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)
+ }
}
}
// verify $emit
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()
verify(emitsDeclarations, nameLiteralNode, vueNode)
}
diff --git a/lib/rules/require-slots-as-functions.js b/lib/rules/require-slots-as-functions.js
index 36a3f9339..658071d22 100644
--- a/lib/rules/require-slots-as-functions.js
+++ b/lib/rules/require-slots-as-functions.js
@@ -33,7 +33,7 @@ module.exports = {
create(context) {
/**
* 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
*/
function verify(node, reportNode) {
@@ -58,6 +58,12 @@ module.exports = {
return
}
+ if (parent.type === 'ChainExpression') {
+ // (this.$slots?.foo).x
+ verify(parent, reportNode)
+ return
+ }
+
if (
// this.$slots.foo.xxx
parent.type === 'MemberExpression' ||
@@ -97,14 +103,11 @@ module.exports = {
return utils.defineVueVisitor(context, {
/** @param {MemberExpression} node */
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
}
if (!utils.isThis(object.object, context)) {
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index a0444db08..ffebef07f 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -48,7 +48,7 @@ function getPropertyNode(obj, name) {
}
/**
- * @param {Expression | Pattern} node
+ * @param {Expression} node
* @returns {string[]}
*/
function getTypes(node) {
@@ -122,10 +122,11 @@ module.exports = {
}
/**
- * @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') {
// Symbol(), Number() ...
if (
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index bf84361fb..6f38166ee 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -56,9 +56,9 @@ module.exports = {
* @param {VAttribute|VDirective} slotAttr node of `slot`
* @param {string | null} slotName name of `slot`
* @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
- * @returns {Fix[]} fix data
+ * @returns {IterableIterator
} fix data
*/
- function fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
+ function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
const element = slotAttr.parent
const scopeAttr = element.attributes.find(
(attr) =>
@@ -78,11 +78,10 @@ module.exports = {
: ''
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
}
/**
* Reports `slot` node
@@ -94,12 +93,12 @@ module.exports = {
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// 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)
}
})
}
@@ -113,15 +112,15 @@ module.exports = {
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
- fix(fixer) {
+ *fix(fixer) {
if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
- return null
+ return
}
const slotName =
slotAttr.value &&
slotAttr.value.expression &&
sourceCode.getText(slotAttr.value.expression).trim()
- return fixSlotToVSlot(fixer, slotAttr, slotName, true)
+ yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
}
})
}
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index e05be5c3f..2afd790b6 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -74,16 +74,17 @@ module.exports = {
context.report({
node: scopeAttr.key,
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)
+ }
})
}
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index 9d563a2dd..ae71dc9de 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -69,7 +69,7 @@ module.exports = {
node: vSlotAttr.key,
messageId: 'forbiddenVSlot',
// fix to use `slot` (downgrade)
- fix: (fixer) => {
+ fix(fixer) {
if (!canConvertToSlot(vSlotAttr)) {
return null
}
diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js
index 04eb79792..4befe9f8c 100644
--- a/lib/rules/v-on-function-call.js
+++ b/lib/rules/v-on-function-call.js
@@ -85,6 +85,10 @@ module.exports = {
if (expression.type !== 'CallExpression' || expression.arguments.length) {
return null
}
+ if (expression.optional) {
+ // Allow optional chaining
+ return null
+ }
const callee = expression.callee
if (callee.type !== 'Identifier') {
return null
diff --git a/lib/rules/valid-v-bind-sync.js b/lib/rules/valid-v-bind-sync.js
index 3799d0895..48452f822 100644
--- a/lib/rules/valid-v-bind-sync.js
+++ b/lib/rules/valid-v-bind-sync.js
@@ -32,7 +32,22 @@ function isValidElement(node) {
}
/**
- * 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.
*/
@@ -40,6 +55,33 @@ function isLhs(node) {
return node.type === 'Identifier' || node.type === 'MemberExpression'
}
+/**
+ * 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
+}
+
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -57,8 +99,12 @@ module.exports = {
messages: {
unexpectedInvalidElement:
"'.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:
"'.sync' modifiers cannot update the iteration variable '{{varName}}' itself."
}
@@ -83,16 +129,33 @@ module.exports = {
})
}
- 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'
+ })
}
for (const reference of node.value.references) {
diff --git a/lib/rules/valid-v-model.js b/lib/rules/valid-v-model.js
index 451787367..adfe9c2da 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -37,7 +37,22 @@ function isValidElement(node) {
}
/**
- * 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.
*/
@@ -45,6 +60,33 @@ function isLhs(node) {
return node.type === 'Identifier' || node.type === 'MemberExpression'
}
+/**
+ * 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.
* @param {string} name The variable name to find.
@@ -76,6 +118,7 @@ function getVariable(name, leafNode) {
// Rule Definition
// ------------------------------------------------------------------------------
+/** @type {RuleModule} */
module.exports = {
meta: {
type: 'problem',
@@ -85,7 +128,25 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/valid-v-model.html'
},
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."
+ }
},
/** @param {RuleContext} context */
create(context) {
@@ -99,8 +160,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message:
- "'v-model' directives aren't supported on <{{name}}> elements.",
+ messageId: 'unexpectedInvalidElement',
data: { name }
})
}
@@ -109,7 +169,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message: "'v-model' directives don't support 'file' input type."
+ messageId: 'unexpectedInputFile'
})
}
@@ -118,7 +178,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message: "'v-model' directives require no argument."
+ messageId: 'unexpectedArgument'
})
}
@@ -127,8 +187,7 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message:
- "'v-model' directives don't support the modifier '{{name}}'.",
+ messageId: 'unexpectedModifier',
data: { name: modifier.name }
})
}
@@ -139,20 +198,32 @@ module.exports = {
context.report({
node,
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'
})
}
@@ -167,8 +238,8 @@ module.exports = {
context.report({
node,
loc: node.loc,
- message:
- "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
+ messageId: 'unexpectedUpdateIterationVariable',
+
data: { varName: id.name }
})
}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 0a6f7bf33..130530526 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -315,7 +315,6 @@ module.exports = {
})
}
},
-
/**
* Checks whether the given value is defined.
* @template T
@@ -323,18 +322,6 @@ module.exports = {
* @returns {v is T}
*/
isDef,
- /**
- * 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.
* @param {VElement} node The element node to get the previous sibling element.
@@ -648,36 +635,6 @@ module.exports = {
isHtmlVoidElementName(name) {
return VOID_ELEMENT_NAMES.has(name)
},
-
- /**
- * 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.
* @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get.
@@ -724,7 +681,7 @@ module.exports = {
type: 'object',
key: prop.key,
propName,
- value: unwrapTypes(prop.value),
+ value: skipTSAsExpression(prop.value),
node: prop
}
}
@@ -732,7 +689,7 @@ module.exports = {
type: 'object',
key: null,
propName: null,
- value: unwrapTypes(prop.value),
+ value: skipTSAsExpression(prop.value),
node: prop
}
})
@@ -795,7 +752,7 @@ module.exports = {
type: 'object',
key: prop.key,
emitName,
- value: unwrapTypes(prop.value),
+ value: skipTSAsExpression(prop.value),
node: prop
}
}
@@ -803,7 +760,7 @@ module.exports = {
type: 'object',
key: null,
emitName: null,
- value: unwrapTypes(prop.value),
+ value: skipTSAsExpression(prop.value),
node: prop
}
})
@@ -862,7 +819,7 @@ module.exports = {
.map((cp) => {
const key = getStaticPropertyName(cp)
/** @type {Expression} */
- const propValue = unwrapTypes(cp.value)
+ const propValue = skipTSAsExpression(cp.value)
/** @type {BlockStatement | null} */
let value = null
@@ -1056,7 +1013,7 @@ module.exports = {
const callee = callExpr.callee
if (callee.type === 'MemberExpression') {
- const calleeObject = unwrapTypes(callee.object)
+ const calleeObject = skipTSAsExpression(callee.object)
if (
calleeObject.type === 'Identifier' &&
@@ -1125,7 +1082,7 @@ module.exports = {
*/
*iterateObjectExpression(node, groupName) {
/** @type {Set | undefined} */
- let usedGetter = new Set()
+ let usedGetter
for (const item of node.properties) {
if (item.type === 'Property') {
const key = item.key
@@ -1313,52 +1270,15 @@ module.exports = {
getMemberChaining(node) {
/** @type {MemberExpression[]} */
const nodes = []
- let n = node
+ let n = skipChainExpression(node)
while (n.type === 'MemberExpression') {
nodes.push(n)
- n = n.object
+ n = skipChainExpression(n.object)
}
return [n, ...nodes.reverse()]
},
- /**
- * 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
* @param {string} a string a to compare
@@ -1418,18 +1338,17 @@ module.exports = {
*/
isPropertyChain,
/**
- * 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.
+ */
+ skipTSAsExpression,
+ /**
+ * Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it.
*/
- unwrapTypes,
+ skipDefaultParamValue,
/**
- * Unwrap AssignmentPattern like "(a = 1) => ret"
- * @param { AssignmentPattern | RestElement | ArrayPattern | ObjectPattern | Identifier } node
- * @return { RestElement | ArrayPattern | ObjectPattern | Identifier}
+ * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
*/
- unwrapAssignmentPattern,
+ skipChainExpression,
/**
* Check whether the given node is `this` or variable that stores `this`.
@@ -1479,6 +1398,7 @@ module.exports = {
findMutating(props) {
/** @type {MemberExpression[]} */
const pathNodes = []
+ /** @type {MemberExpression | Identifier | ChainExpression} */
let node = props
let target = node.parent
while (true) {
@@ -1499,10 +1419,9 @@ module.exports = {
pathNodes
}
} 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 (
callName &&
/^push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill$/u.exec(
@@ -1525,6 +1444,10 @@ module.exports = {
target = target.parent
continue // loop
}
+ } else if (target.type === 'ChainExpression') {
+ node = target
+ target = target.parent
+ continue // loop
}
return null
@@ -1660,14 +1583,12 @@ function findProperty(node, name, filter) {
* @returns {prop is Property}
*/
(prop) =>
- prop.type === 'Property' &&
- getStaticPropertyName(prop) === name &&
- filter(prop)
+ isProperty(prop) && getStaticPropertyName(prop) === name && filter(prop)
: /**
* @param {Property | SpreadElement} prop
* @returns {prop is Property}
*/
- (prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name
+ (prop) => isProperty(prop) && getStaticPropertyName(prop) === name
return node.properties.find(predicate) || null
}
@@ -1685,14 +1606,15 @@ function findAssignmentProperty(node, name, filter) {
* @returns {prop is AssignmentProperty}
*/
(prop) =>
- prop.type === 'Property' &&
+ isAssignmentProperty(prop) &&
getStaticPropertyName(prop) === name &&
filter(prop)
: /**
* @param {AssignmentProperty | RestElement} prop
* @returns {prop is AssignmentProperty}
*/
- (prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name
+ (prop) =>
+ isAssignmentProperty(prop) && getStaticPropertyName(prop) === name
return node.properties.find(predicate) || null
}
@@ -1722,20 +1644,21 @@ function isVElement(node) {
}
/**
- * 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) {
return node
}
// @ts-expect-error
if (node.type === 'TSAsExpression') {
// @ts-expect-error
- return unwrapTypes(node.expression)
+ return skipTSAsExpression(node.expression)
}
+ // @ts-expect-error
return node
}
@@ -1766,18 +1689,40 @@ function isPropertyChain(prop, node) {
}
/**
- * 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
+}
+
+/**
+ * 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
}
@@ -1879,7 +1824,7 @@ function isVueComponent(node) {
const callee = node.callee
if (callee.type === 'MemberExpression') {
- const calleeObject = unwrapTypes(callee.object)
+ const calleeObject = skipTSAsExpression(callee.object)
if (calleeObject.type === 'Identifier') {
const propName = getStaticPropertyName(callee)
@@ -1933,7 +1878,8 @@ function isVueComponent(node) {
function isObjectArgument(node) {
return (
node.arguments.length > 0 &&
- unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
+ skipTSAsExpression(node.arguments.slice(-1)[0]).type ===
+ 'ObjectExpression'
)
}
}
@@ -1951,7 +1897,7 @@ function isVueInstance(node) {
callee.type === 'Identifier' &&
callee.name === 'Vue' &&
node.arguments.length &&
- unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
+ skipTSAsExpression(node.arguments[0]).type === 'ObjectExpression'
)
}
@@ -1971,7 +1917,7 @@ function getVueObjectType(context, node) {
const filePath = context.getFilename()
if (
isVueComponentFile(parent, filePath) &&
- unwrapTypes(parent.declaration) === node
+ skipTSAsExpression(parent.declaration) === node
) {
return 'export'
}
@@ -1979,13 +1925,16 @@ function getVueObjectType(context, node) {
// Vue.component('xxx', {}) || component('xxx', {})
if (
isVueComponent(parent) &&
- unwrapTypes(parent.arguments.slice(-1)[0]) === node
+ skipTSAsExpression(parent.arguments.slice(-1)[0]) === node
) {
return 'definition'
}
} else if (parent.type === 'NewExpression') {
// new Vue({})
- if (isVueInstance(parent) && unwrapTypes(parent.arguments[0]) === node) {
+ if (
+ isVueInstance(parent) &&
+ skipTSAsExpression(parent.arguments[0]) === node
+ ) {
return 'instance'
}
}
diff --git a/package.json b/package.json
index f1a5d94d4..2dacb75b8 100644
--- a/package.json
+++ b/package.json
@@ -50,10 +50,10 @@
"node": ">=8.10"
},
"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",
"semver": "^7.3.2",
"vue-eslint-parser": "^7.1.0"
diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js
index b767223e2..934bb079f 100644
--- a/tests/lib/rules/custom-event-name-casing.js
+++ b/tests/lib/rules/custom-event-name-casing.js
@@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/custom-event-name-casing')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
- ecmaVersion: 2019,
+ ecmaVersion: 2020,
sourceType: 'module'
}
})
@@ -216,6 +216,66 @@ tester.run('custom-event-name-casing', rule, {
endColumn: 32
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ "Custom event name 'fooBar' must be kebab-case.",
+ "Custom event name 'barBaz' must be kebab-case.",
+ "Custom event name 'bazQux' must be kebab-case."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ "Custom event name 'fooBar' must be kebab-case.",
+ "Custom event name 'barBaz' must be kebab-case.",
+ "Custom event name 'bazQux' must be kebab-case."
+ ]
}
]
})
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
index de6769091..8ea81b5cb 100644
--- a/tests/lib/rules/no-async-in-computed-properties.js
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-async-in-computed-properties')
const RuleTester = require('eslint').RuleTester
const parserOptions = {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
}
@@ -302,6 +302,34 @@ ruleTester.run('no-async-in-computed-properties', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ computed: {
+ foo: function () {
+ return bar?.then?.(response => {})
+ }
+ }
+ }
+ `,
+ parserOptions,
+ errors: ['Unexpected asynchronous action in "foo" computed property.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ computed: {
+ foo: function () {
+ return (bar?.then)?.(response => {})
+ }
+ }
+ }
+ `,
+ parserOptions,
+ errors: ['Unexpected asynchronous action in "foo" computed property.']
+ },
{
filename: 'test.vue',
code: `
@@ -551,6 +579,66 @@ ruleTester.run('no-async-in-computed-properties', rule, {
line: 12
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ computed: {
+ foo: function () {
+ setTimeout?.(() => { }, 0)
+ window?.setTimeout?.(() => { }, 0)
+ setInterval(() => { }, 0)
+ window?.setInterval?.(() => { }, 0)
+ setImmediate?.(() => { })
+ window?.setImmediate?.(() => { })
+ requestAnimationFrame?.(() => {})
+ window?.requestAnimationFrame?.(() => {})
+ }
+ }
+ }
+ `,
+ parserOptions,
+ errors: [
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ computed: {
+ foo: function () {
+ setTimeout?.(() => { }, 0)
+ ;(window?.setTimeout)?.(() => { }, 0)
+ setInterval(() => { }, 0)
+ ;(window?.setInterval)?.(() => { }, 0)
+ setImmediate?.(() => { })
+ ;(window?.setImmediate)?.(() => { })
+ requestAnimationFrame?.(() => {})
+ ;(window?.requestAnimationFrame)?.(() => {})
+ }
+ }
+ }
+ `,
+ parserOptions,
+ errors: [
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.',
+ 'Unexpected timed function in "foo" computed property.'
+ ]
}
]
})
diff --git a/tests/lib/rules/no-deprecated-dollar-listeners-api.js b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
index deaff0758..8644d13aa 100644
--- a/tests/lib/rules/no-deprecated-dollar-listeners-api.js
+++ b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
valid: [
@@ -240,6 +240,30 @@ ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
messageId: 'deprecated'
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'deprecated'
+ },
+ {
+ messageId: 'deprecated'
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
index 303e5de99..a29211a22 100644
--- a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
+++ b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, {
valid: [
@@ -283,6 +283,41 @@ ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, {
messageId: 'deprecated'
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ errors: [
+ {
+ messageId: 'deprecated'
+ },
+ {
+ messageId: 'deprecated'
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-deprecated-events-api.js b/tests/lib/rules/no-deprecated-events-api.js
index b52651aff..3022690b1 100644
--- a/tests/lib/rules/no-deprecated-events-api.js
+++ b/tests/lib/rules/no-deprecated-events-api.js
@@ -13,7 +13,7 @@ const rule = require('../../../lib/rules/no-deprecated-events-api')
const RuleTester = require('eslint').RuleTester
const parserOptions = {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
}
@@ -113,6 +113,20 @@ ruleTester.run('no-deprecated-events-api', rule, {
}
`,
parserOptions
+ },
+ {
+ filename: 'test.js',
+ code: `
+ app.component('some-comp', {
+ mounted () {
+ // It is OK because checking whether it is deprecated.
+ this.$on?.('start', foo)
+ this.$off?.('start', foo)
+ this.$once?.('start', foo)
+ }
+ })
+ `,
+ parserOptions
}
],
@@ -195,6 +209,42 @@ ruleTester.run('no-deprecated-events-api', rule, {
line: 5
}
]
+ },
+ {
+ filename: 'test.js',
+ code: `
+ app.component('some-comp', {
+ mounted () {
+ this?.$on('start')
+ this?.$off('start')
+ this?.$once('start')
+ }
+ })
+ `,
+ parserOptions,
+ errors: [
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
+ ]
+ },
+ {
+ filename: 'test.js',
+ code: `
+ app.component('some-comp', {
+ mounted () {
+ ;(this?.$on)('start')
+ ;(this?.$off)('start')
+ ;(this?.$once)('start')
+ }
+ })
+ `,
+ parserOptions,
+ errors: [
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+ 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
+ ]
}
]
})
diff --git a/tests/lib/rules/no-deprecated-vue-config-keycodes.js b/tests/lib/rules/no-deprecated-vue-config-keycodes.js
index eebd7943c..dae0ec800 100644
--- a/tests/lib/rules/no-deprecated-vue-config-keycodes.js
+++ b/tests/lib/rules/no-deprecated-vue-config-keycodes.js
@@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2015 }
+ parserOptions: { ecmaVersion: 2020 }
})
ruleTester.run('no-deprecated-vue-config-keycodes', rule, {
@@ -51,6 +51,16 @@ ruleTester.run('no-deprecated-vue-config-keycodes', rule, {
endColumn: 20
}
]
+ },
+ {
+ filename: 'test.js',
+ code: 'Vue?.config?.keyCodes',
+ errors: ['`Vue.config.keyCodes` are deprecated.']
+ },
+ {
+ filename: 'test.js',
+ code: '(Vue?.config)?.keyCodes',
+ errors: ['`Vue.config.keyCodes` are deprecated.']
}
]
})
diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js
index dd1660790..17dfada47 100644
--- a/tests/lib/rules/no-lifecycle-after-await.js
+++ b/tests/lib/rules/no-lifecycle-after-await.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-lifecycle-after-await')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
tester.run('no-lifecycle-after-await', rule, {
@@ -204,6 +204,26 @@ tester.run('no-lifecycle-after-await', rule, {
line: 18
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'forbidden'
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-multiple-slot-args.js b/tests/lib/rules/no-multiple-slot-args.js
index fb42767b9..0720a951f 100644
--- a/tests/lib/rules/no-multiple-slot-args.js
+++ b/tests/lib/rules/no-multiple-slot-args.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
ruleTester.run('no-multiple-slot-args', rule, {
valid: [
@@ -109,6 +109,90 @@ ruleTester.run('no-multiple-slot-args', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'Unexpected multiple arguments.',
+ 'Unexpected multiple arguments.'
+ ]
+ },
{
filename: 'test.vue',
code: `
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index 1e9e09737..e46ae0fd2 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -343,6 +343,52 @@ ruleTester.run('no-mutating-props', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ 'Unexpected mutation of "prop1" prop.',
+ 'Unexpected mutation of "prop5" prop.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ 'Unexpected mutation of "prop1" prop.',
+ 'Unexpected mutation of "prop2" prop.',
+ 'Unexpected mutation of "prop3" prop.'
+ ]
+ },
{
filename: 'test.vue',
code: `
@@ -419,6 +465,28 @@ ruleTester.run('no-mutating-props', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'Unexpected mutation of "foo" prop.',
+ 'Unexpected mutation of "bar" prop.',
+ 'Unexpected mutation of "baz" prop.'
+ ]
+ },
{
filename: 'test.vue',
code: `
diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js
index ca553e35c..50cfd3249 100644
--- a/tests/lib/rules/no-ref-as-operand.js
+++ b/tests/lib/rules/no-ref-as-operand.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-ref-as-operand')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
tester.run('no-ref-as-operand', rule, {
@@ -492,6 +492,20 @@ tester.run('no-ref-as-operand', rule, {
messageId: 'requireDotValue'
}
]
+ },
+ {
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'requireDotValue'
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js
index c0e35df06..781a3ce08 100644
--- a/tests/lib/rules/no-setup-props-destructure.js
+++ b/tests/lib/rules/no-setup-props-destructure.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-setup-props-destructure')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
tester.run('no-setup-props-destructure', rule, {
@@ -342,6 +342,38 @@ tester.run('no-setup-props-destructure', rule, {
}
]
},
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'getProperty',
+ line: 5
+ },
+ {
+ messageId: 'getProperty',
+ line: 6
+ },
+ {
+ messageId: 'getProperty',
+ line: 7
+ }
+ ]
+ },
{
filename: 'test.vue',
code: `
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index 40caaf0c9..5aefc95c9 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-side-effects-in-computed-properties'
const RuleTester = require('eslint').RuleTester
const parserOptions = {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
}
@@ -338,6 +338,27 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
message: 'Unexpected side effect in "test1" computed property.'
}
]
+ },
+ {
+ code: `Vue.component('test', {
+ computed: {
+ test1() {
+ return this?.something?.reverse?.()
+ },
+ test2() {
+ return (this?.something)?.reverse?.()
+ },
+ test3() {
+ return (this?.something?.reverse)?.()
+ },
+ }
+ })`,
+ parserOptions,
+ errors: [
+ 'Unexpected side effect in "test1" computed property.',
+ 'Unexpected side effect in "test2" computed property.',
+ 'Unexpected side effect in "test3" computed property.'
+ ]
}
]
})
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index 355fb8df7..4319d14fc 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -995,6 +995,41 @@ tester.run('no-unused-properties', rule, {
props: [, 'count']
}
+ `
+ },
+ // optional chaining
+ {
+ filename: 'test.vue',
+ code: `
+ `
+ },
+ {
+ filename: 'test.js',
+ code: `
+ Vue.component('MyButton', {
+ functional: true,
+ props: ['foo', 'bar'],
+ render: function (createElement, ctx) {
+ const a = ctx
+ const b = a?.props?.foo
+ const c = (a?.props)?.bar
+ }
+ })
`
}
],
diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js
index 9bc7c04e6..c7cf73150 100644
--- a/tests/lib/rules/no-watch-after-await.js
+++ b/tests/lib/rules/no-watch-after-await.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-watch-after-await')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
tester.run('no-watch-after-await', rule, {
@@ -97,6 +97,27 @@ tester.run('no-watch-after-await', rule, {
Vue.component('test', {
el: foo()
})`
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
}
],
invalid: [
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 5c99608fc..76d510964 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -879,6 +879,7 @@ ruleTester.run('order-in-components', rule, {
testYield: function* () {},
testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
testNullish: a ?? b,
+ testOptionalChaining: a?.b?.c,
name: 'burger',
};
`,
@@ -897,13 +898,14 @@ ruleTester.run('order-in-components', rule, {
testYield: function* () {},
testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
testNullish: a ?? b,
+ testOptionalChaining: a?.b?.c,
};
`,
errors: [
{
message:
'The "name" property should be above the "data" property on line 3.',
- line: 14
+ line: 15
}
]
}
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index cdc0f4170..dcd551560 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -11,7 +11,7 @@
const rule = require('../../../lib/rules/require-default-prop')
const RuleTester = require('eslint').RuleTester
const parserOptions = {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
}
@@ -148,7 +148,8 @@ ruleTester.run('require-default-prop', rule, {
props: {
bar,
baz: prop,
- bar1: foo()
+ baz1: prop.foo,
+ bar2: foo()
}
}
`
@@ -285,7 +286,7 @@ ruleTester.run('require-default-prop', rule, {
]
},
- // computed propertys
+ // computed properties
{
filename: 'test.vue',
code: `
@@ -370,6 +371,22 @@ ruleTester.run('require-default-prop', rule, {
}
`,
errors: ["Prop 'foo' requires default value to be set."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ props: {
+ bar,
+ baz: prop?.foo,
+ bar1: foo?.(),
+ }
+ }
+ `,
+ errors: [
+ "Prop 'baz' requires default value to be set.",
+ "Prop 'bar1' requires default value to be set."
+ ]
}
]
})
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
index cef95dd75..d59d78600 100644
--- a/tests/lib/rules/require-explicit-emits.js
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/require-explicit-emits')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
- ecmaVersion: 2019,
+ ecmaVersion: 2020,
sourceType: 'module'
}
})
@@ -1514,6 +1514,43 @@ emits: {'foo': null}
]
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'The "foo" event has been triggered but not declared on `emits` option.',
+ 'The "bar" event has been triggered but not declared on `emits` option.'
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ 'The "foo" event has been triggered but not declared on `emits` option.',
+ 'The "bar" event has been triggered but not declared on `emits` option.'
+ ]
}
]
})
diff --git a/tests/lib/rules/require-slots-as-functions.js b/tests/lib/rules/require-slots-as-functions.js
index 9d79a1ec7..ea83b5696 100644
--- a/tests/lib/rules/require-slots-as-functions.js
+++ b/tests/lib/rules/require-slots-as-functions.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})
ruleTester.run('require-slots-as-functions', rule, {
valid: [
@@ -86,29 +86,27 @@ ruleTester.run('require-slots-as-functions', rule, {
}
]
},
-
{
filename: 'test.vue',
code: `
`,
errors: [
- 'Property in `$slots` should be used as function.',
- 'Property in `$slots` should be used as function.',
- 'Property in `$slots` should be used as function.'
+ { messageId: 'unexpected', line: 5 },
+ { messageId: 'unexpected', line: 7 },
+ { messageId: 'unexpected', line: 9 }
]
}
]
diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js
index 2617e1460..58adccadf 100644
--- a/tests/lib/rules/require-valid-default-prop.js
+++ b/tests/lib/rules/require-valid-default-prop.js
@@ -195,6 +195,18 @@ ruleTester.run('require-valid-default-prop', rule, {
}
}`,
parserOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `export default {
+ props: {
+ foo: {
+ type: Number,
+ default: Number?.()
+ }
+ }
+ }`,
+ parserOptions
}
],
@@ -756,6 +768,19 @@ ruleTester.run('require-valid-default-prop', rule, {
line: 11
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default {
+ props: {
+ foo: {
+ type: String,
+ default: Number?.()
+ }
+ }
+ }`,
+ parserOptions,
+ errors: errorMessage('string')
}
]
})
diff --git a/tests/lib/rules/this-in-template.js b/tests/lib/rules/this-in-template.js
index 2d7bd46c1..5f36b570e 100644
--- a/tests/lib/rules/this-in-template.js
+++ b/tests/lib/rules/this-in-template.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2015 }
+ parserOptions: { ecmaVersion: 2020 }
})
function createValidTests(prefix, options) {
@@ -188,7 +188,8 @@ ruleTester.run('this-in-template', rule, {
valid: ['', ' ', '
']
.concat(createValidTests('', []))
.concat(createValidTests('', ['never']))
- .concat(createValidTests('this.', ['always'])),
+ .concat(createValidTests('this.', ['always']))
+ .concat(createValidTests('this?.', ['always'])),
invalid: []
.concat(
createInvalidTests(
@@ -196,6 +197,12 @@ ruleTester.run('this-in-template', rule, {
[],
"Unexpected usage of 'this'.",
'ThisExpression'
+ ),
+ createInvalidTests(
+ 'this?.',
+ [],
+ "Unexpected usage of 'this'.",
+ 'ThisExpression'
)
)
.concat(
@@ -204,6 +211,12 @@ ruleTester.run('this-in-template', rule, {
['never'],
"Unexpected usage of 'this'.",
'ThisExpression'
+ ),
+ createInvalidTests(
+ 'this?.',
+ ['never'],
+ "Unexpected usage of 'this'.",
+ 'ThisExpression'
)
)
.concat(
diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js
index b78a92370..4ba6f5b00 100644
--- a/tests/lib/rules/v-on-function-call.js
+++ b/tests/lib/rules/v-on-function-call.js
@@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/v-on-function-call')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2015 }
+ parserOptions: { ecmaVersion: 2020 }
})
tester.run('v-on-function-call', rule, {
@@ -106,6 +106,11 @@ tester.run('v-on-function-call', rule, {
`,
options: ['never', { ignoreIncludesComment: true }]
+ },
+ {
+ filename: 'test.vue',
+ code: '
',
+ options: ['never']
}
],
invalid: [
diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js
index ea8bdf333..dfaf161ea 100644
--- a/tests/lib/rules/valid-v-bind-sync.js
+++ b/tests/lib/rules/valid-v-bind-sync.js
@@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/valid-v-bind-sync')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2015 }
+ parserOptions: { ecmaVersion: 2020 }
})
tester.run('valid-v-bind-sync', rule, {
@@ -350,6 +350,42 @@ tester.run('valid-v-bind-sync', rule, {
errors: [
"'.sync' modifiers require the attribute value which is valid as LHS."
]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: [
+ "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: [
+ "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: [
+ "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'.sync' modifier has potential null object property access."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'.sync' modifier has potential null object property access."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'.sync' modifier has potential null object property access."]
}
]
})
diff --git a/tests/lib/rules/valid-v-model.js b/tests/lib/rules/valid-v-model.js
index 9e78ef47a..2369fe4df 100644
--- a/tests/lib/rules/valid-v-model.js
+++ b/tests/lib/rules/valid-v-model.js
@@ -18,7 +18,7 @@ const rule = require('../../../lib/rules/valid-v-model')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
- parserOptions: { ecmaVersion: 2015 }
+ parserOptions: { ecmaVersion: 2020 }
})
tester.run('valid-v-model', rule, {
@@ -232,6 +232,36 @@ tester.run('valid-v-model', rule, {
filename: 'empty-value.vue',
code: ' ',
errors: ["'v-model' directives require that attribute value."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["Optional chaining cannot appear in 'v-model' directives."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["Optional chaining cannot appear in 'v-model' directives."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["Optional chaining cannot appear in 'v-model' directives."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'v-model' directive has potential null object property access."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'v-model' directive has potential null object property access."]
+ },
+ {
+ filename: 'test.vue',
+ code: ' ',
+ errors: ["'v-model' directive has potential null object property access."]
}
]
})
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index d1b8c076d..fe5bf77fa 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -1,44 +1,19 @@
'use strict'
const babelEslint = require('babel-eslint')
+const espree = require('espree')
const utils = require('../../../lib/utils/index')
const chai = require('chai')
const assert = chai.assert
-describe('parseMemberExpression', () => {
- let node
-
- const parse = function (code) {
- return babelEslint.parse(code).body[0].expression
- }
-
- it('should parse member expression', () => {
- node = parse('this.some.nested.property')
- assert.deepEqual(utils.parseMemberExpression(node), [
- 'this',
- 'some',
- 'nested',
- 'property'
- ])
-
- node = parse('another.property')
- assert.deepEqual(utils.parseMemberExpression(node), ['another', 'property'])
-
- node = parse('this.something')
- assert.deepEqual(utils.parseMemberExpression(node), ['this', 'something'])
- })
-})
-
describe('getComputedProperties', () => {
- let node
-
const parse = function (code) {
return babelEslint.parse(code).body[0].declarations[0].init
}
it('should return empty array when there is no computed property', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
data() {
return {}
@@ -49,7 +24,7 @@ describe('getComputedProperties', () => {
})
it('should return computed properties', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
data() {
return {}
@@ -93,7 +68,7 @@ describe('getComputedProperties', () => {
})
it('should not collide with object spread operator', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
computed: {
...mapGetters(['test']),
@@ -115,7 +90,7 @@ describe('getComputedProperties', () => {
})
it('should not collide with object spread operator inside CP', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
computed: {
foo: {
@@ -138,83 +113,175 @@ describe('getComputedProperties', () => {
})
describe('getStaticPropertyName', () => {
- let node
-
const parse = function (code) {
return babelEslint.parse(code).body[0].declarations[0].init
}
it('should parse property expression with identifier', () => {
- node = parse(`const test = { computed: { } }`)
+ const node = parse(`const test = { computed: { } }`)
const parsed = utils.getStaticPropertyName(node.properties[0])
assert.ok(parsed === 'computed')
})
it('should parse property expression with literal', () => {
- node = parse(`const test = { ['computed'] () {} }`)
+ const node = parse(`const test = { ['computed'] () {} }`)
const parsed = utils.getStaticPropertyName(node.properties[0])
assert.ok(parsed === 'computed')
})
it('should parse property expression with template literal', () => {
- node = parse(`const test = { [\`computed\`] () {} }`)
+ const node = parse(`const test = { [\`computed\`] () {} }`)
const parsed = utils.getStaticPropertyName(node.properties[0])
assert.ok(parsed === 'computed')
})
- // it('should parse identifier', () => {
- // node = parse(`const test = { computed: { } }`)
+})
+
+describe('getStringLiteralValue', () => {
+ const parse = function (code) {
+ return babelEslint.parse(code).body[0].declarations[0].init
+ }
- // const parsed = utils.getStaticPropertyName(node.properties[0].key)
- // assert.ok(parsed === 'computed')
- // })
it('should parse literal', () => {
- node = parse(`const test = { ['computed'] () {} }`)
+ const node = parse(`const test = { ['computed'] () {} }`)
const parsed = utils.getStringLiteralValue(node.properties[0].key)
assert.ok(parsed === 'computed')
})
it('should parse template literal', () => {
- node = parse(`const test = { [\`computed\`] () {} }`)
+ const node = parse(`const test = { [\`computed\`] () {} }`)
const parsed = utils.getStringLiteralValue(node.properties[0].key)
assert.ok(parsed === 'computed')
})
})
-describe('parseMemberOrCallExpression', () => {
- let node
-
+describe('getMemberChaining', () => {
const parse = function (code) {
- return babelEslint.parse(code).body[0].declarations[0].init
+ return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+ .init
}
- it('should parse CallExpression', () => {
- node = parse(
- `const test = this.lorem['ipsum'].map(d => d.id).filter((a, b) => a > b).reduce((acc, d) => acc + d, 0)`
+ const jsonIgnoreKeys = ['expression', 'object']
+
+ it('should parse MemberExpression', () => {
+ const node = parse(`const test = this.lorem['ipsum'].foo.bar`)
+ const parsed = utils.getMemberChaining(node)
+ assert.equal(
+ nodeToJson(parsed, jsonIgnoreKeys),
+ nodeToJson([
+ {
+ type: 'ThisExpression'
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'lorem'
+ },
+ computed: false,
+ optional: false
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Literal',
+ value: 'ipsum',
+ raw: "'ipsum'"
+ },
+ computed: true,
+ optional: false
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'foo'
+ },
+ computed: false,
+ optional: false
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'bar'
+ },
+ computed: false,
+ optional: false
+ }
+ ])
)
- const parsed = utils.parseMemberOrCallExpression(node)
- assert.equal(parsed, 'this.lorem[].map().filter().reduce()')
})
- it('should parse MemberExpression', () => {
- node = parse(
- `const test = this.lorem['ipsum'][0].map(d => d.id).dolor.reduce((acc, d) => acc + d, 0).sit`
+ it('should parse optional Chaining ', () => {
+ const node = parse(`const test = (this?.lorem)['ipsum']?.[0]?.foo?.bar`)
+ const parsed = utils.getMemberChaining(node)
+ assert.equal(
+ nodeToJson(parsed, jsonIgnoreKeys),
+ nodeToJson([
+ {
+ type: 'ThisExpression'
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'lorem'
+ },
+ computed: false,
+ optional: true
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Literal',
+ value: 'ipsum',
+ raw: "'ipsum'"
+ },
+ computed: true,
+ optional: false
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Literal',
+ value: 0,
+ raw: '0'
+ },
+ computed: true,
+ optional: true
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'foo'
+ },
+ computed: false,
+ optional: true
+ },
+ {
+ type: 'MemberExpression',
+ property: {
+ type: 'Identifier',
+ name: 'bar'
+ },
+ computed: false,
+ optional: true
+ }
+ ])
)
- const parsed = utils.parseMemberOrCallExpression(node)
- assert.equal(parsed, 'this.lorem[][].map().dolor.reduce().sit')
})
})
describe('getRegisteredComponents', () => {
- let node
-
const parse = function (code) {
return babelEslint.parse(code).body[0].declarations[0].init
}
it('should return empty array when there are no components registered', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
}`)
@@ -222,7 +289,7 @@ describe('getRegisteredComponents', () => {
})
it('should return an array with all registered components', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
components: {
...test,
@@ -249,7 +316,7 @@ describe('getRegisteredComponents', () => {
})
it('should return an array of only components whose names can be identified', () => {
- node = parse(`const test = {
+ const node = parse(`const test = {
name: 'test',
components: {
...test,
@@ -269,15 +336,13 @@ describe('getRegisteredComponents', () => {
})
describe('getComponentProps', () => {
- let props
-
const parse = function (code) {
const data = babelEslint.parse(code).body[0].declarations[0].init
return utils.getComponentProps(data)
}
it('should return empty array when there is no component props', () => {
- props = parse(`const test = {
+ const props = parse(`const test = {
name: 'test',
data() {
return {}
@@ -288,7 +353,7 @@ describe('getComponentProps', () => {
})
it('should return empty array when component props is empty array', () => {
- props = parse(`const test = {
+ const props = parse(`const test = {
name: 'test',
props: []
}`)
@@ -297,7 +362,7 @@ describe('getComponentProps', () => {
})
it('should return empty array when component props is empty object', () => {
- props = parse(`const test = {
+ const props = parse(`const test = {
name: 'test',
props: {}
}`)
@@ -306,7 +371,7 @@ describe('getComponentProps', () => {
})
it('should return computed props', () => {
- props = parse(`const test = {
+ const props = parse(`const test = {
name: 'test',
...test,
data() {
@@ -341,7 +406,7 @@ describe('getComponentProps', () => {
})
it('should return computed from array props', () => {
- props = parse(`const test = {
+ const props = parse(`const test = {
name: 'test',
data() {
return {}
@@ -382,3 +447,15 @@ describe('editdistance', () => {
assert.equal(editDistance('computed', 'computd'), 1)
})
})
+function nodeToJson(nodes, ignores = []) {
+ return JSON.stringify(nodes, replacer, 2)
+ function replacer(key, value) {
+ if (key === 'parent' || key === 'start' || key === 'end') {
+ return undefined
+ }
+ if (ignores.includes(key)) {
+ return undefined
+ }
+ return value
+ }
+}
diff --git a/tools/update-lib-configs.js b/tools/update-lib-configs.js
index 5379decb6..b5b2488ca 100644
--- a/tools/update-lib-configs.js
+++ b/tools/update-lib-configs.js
@@ -49,7 +49,7 @@ function formatCategory(category) {
module.exports = {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
- ecmaVersion: 2018,
+ ecmaVersion: 2020,
sourceType: 'module'
},
env: {