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. `
` @@ -36,6 +37,9 @@ This rule reports `.sync` modifier on `v-bind` directives in the following cases + + + diff --git a/docs/rules/valid-v-model.md b/docs/rules/valid-v-model.md index f3a0afca3..345e3bf9d 100644 --- a/docs/rules/valid-v-model.md +++ b/docs/rules/valid-v-model.md @@ -18,7 +18,8 @@ This rule reports `v-model` directives in the following cases: - The directive used on HTMLElement has an argument. E.g. `` - The directive used on HTMLElement has modifiers which are not supported. E.g. `` - The directive does not have that attribute value. E.g. `` -- The directive does not have the attribute value which is valid as LHS. E.g. `` +- The directive does not have the attribute value which is valid as LHS. E.g. ``, `` +- The directive has potential null object property access. E.g. `` - The directive is on unsupported elements. E.g. `
` - The directive is on `` elements which their types are `file`. E.g. `` - The directive's reference is iteration variables. E.g. `
` @@ -44,6 +45,8 @@ This rule reports `v-model` directives in the following cases: + +
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, ` { + 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: {