Skip to content

Commit

Permalink
Supports Optional Chaining (#1209)
Browse files Browse the repository at this point in the history
* Supports Optional Chaining

* Refactored to resemble eslint core util function naming. e.g. unwrap -> skip

* update config

* upgrade eslint-utils

* Change the supported version of ESLint from 6.0.0 to 6.2.0.
  • Loading branch information
ota-meshi committed Jul 19, 2020
1 parent 8cabc18 commit ce38da7
Show file tree
Hide file tree
Showing 60 changed files with 1,307 additions and 378 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .eslintrc.js
Expand Up @@ -135,7 +135,9 @@ module.exports = {
{
pattern: `https://eslint.vuejs.org/rules/{{name}}.html`
}
]
],

'eslint-plugin/fixer-return': 'off'
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion docs/.vuepress/components/eslint-code-block.vue
Expand Up @@ -89,7 +89,7 @@ export default {
rules: this.rules,
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2019,
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
Expand Down
6 changes: 5 additions & 1 deletion docs/rules/valid-v-bind-sync.md
Expand Up @@ -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. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`
- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`, `<MyComponent v-bind:aaa.sync="a?.b" />`
- The `.sync` modifier has potential null object property access. E.g. `<MyComponent v-bind:aaa.sync="(a?.b).c" />`
- The `.sync` modifier is on non Vue-components. E.g. `<input v-bind:aaa.sync="foo"></div>`
- The `.sync` modifier's reference is iteration variables. E.g. `<div v-for="x in list"><MyComponent v-bind:aaa.sync="x" /></div>`

Expand All @@ -36,6 +37,9 @@ This rule reports `.sync` modifier on `v-bind` directives in the following cases
<MyComponent v-bind:aaa.sync="foo + bar" />
<MyComponent :aaa.sync="foo + bar" />
<MyComponent :aaa.sync="a?.b.c" />
<MyComponent :aaa.sync="(a?.b).c" />
<input v-bind:aaa.sync="foo">
<input :aaa.sync="foo">
Expand Down
5 changes: 4 additions & 1 deletion docs/rules/valid-v-model.md
Expand Up @@ -18,7 +18,8 @@ This rule reports `v-model` directives in the following cases:
- The directive used on HTMLElement has an argument. E.g. `<input v-model:aaa="foo">`
- The directive used on HTMLElement has modifiers which are not supported. E.g. `<input v-model.bbb="foo">`
- The directive does not have that attribute value. E.g. `<input v-model>`
- The directive does not have the attribute value which is valid as LHS. E.g. `<input v-model="foo() + bar()">`
- The directive does not have the attribute value which is valid as LHS. E.g. `<input v-model="foo() + bar()">`, `<input v-model="a?.b">`
- The directive has potential null object property access. E.g. `<input v-model="(a?.b).c">`
- The directive is on unsupported elements. E.g. `<div v-model="foo"></div>`
- The directive is on `<input>` elements which their types are `file`. E.g. `<input type="file" v-model="foo">`
- The directive's reference is iteration variables. E.g. `<div v-for="x in list"><input type="file" v-model="x"></div>`
Expand All @@ -44,6 +45,8 @@ This rule reports `v-model` directives in the following cases:
<input v-model:aaa="foo">
<input v-model.bbb="foo">
<input v-model="foo + bar">
<input v-model="a?.b.c">
<input v-model="(a?.b).c">
<div v-model="foo"/>
<div v-for="todo in todos">
<input v-model="todo">
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/README.md
Expand Up @@ -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
:::

Expand Down
2 changes: 1 addition & 1 deletion lib/configs/base.js
Expand Up @@ -6,7 +6,7 @@
module.exports = {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2018,
ecmaVersion: 2020,
sourceType: 'module'
},
env: {
Expand Down
13 changes: 5 additions & 8 deletions lib/rules/component-name-in-template-casing.js
Expand Up @@ -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}`)
]
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/custom-event-name-casing.js
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/html-self-closing.js
Expand Up @@ -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') {
Expand All @@ -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') {
Expand Down
33 changes: 16 additions & 17 deletions lib/rules/no-async-in-computed-properties.js
Expand Up @@ -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
)
}
Expand All @@ -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
Expand Down
25 changes: 19 additions & 6 deletions lib/rules/no-deprecated-events-api.js
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-deprecated-v-bind-sync.js
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-deprecated-v-on-number-modifiers.js
Expand Up @@ -47,7 +47,7 @@ module.exports = {
context.report({
node: modifier,
messageId: 'numberModifierIsDeprecated',
fix: (fixer) => {
fix(fixer) {
const key = keyCodeToKey[keyCodes]
if (!key) return null

Expand Down
4 changes: 3 additions & 1 deletion lib/rules/no-deprecated-vue-config-keycodes.js
Expand Up @@ -4,6 +4,8 @@
*/
'use strict'

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -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' ||
Expand Down
9 changes: 3 additions & 6 deletions lib/rules/no-multiple-slot-args.js
Expand Up @@ -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)) {
Expand Down
10 changes: 5 additions & 5 deletions lib/rules/no-setup-props-destructure.js
Expand Up @@ -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')
Expand All @@ -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
Expand Down
10 changes: 9 additions & 1 deletion lib/rules/no-unused-properties.js
Expand Up @@ -221,7 +221,7 @@ function getObjectPatternPropertyPatternTracker(pattern) {
}

/**
* @param {Identifier | MemberExpression | ThisExpression} node
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
* @param {RuleContext} context
* @returns {UsedProps}
*/
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-useless-mustaches.js
Expand Up @@ -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'))
}
})
}
Expand Down
11 changes: 5 additions & 6 deletions lib/rules/no-useless-v-bind.js
Expand Up @@ -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]
Expand All @@ -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, '&quot;')
Expand All @@ -136,10 +138,7 @@ module.exports = {
.replace(DOUBLE_QUOTES_RE, '&quot;')
.replace(SINGLE_QUOTES_RE, '&apos;')
}
return [
fixer.removeRange(keyDirectiveRange),
fixer.replaceText(expression, attrValue)
]
yield fixer.replaceText(expression, attrValue)
}
})
}
Expand Down

0 comments on commit ce38da7

Please sign in to comment.