Skip to content

Commit ce38da7

Browse files
authored
Supports Optional Chaining (#1209)
* 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.
1 parent 8cabc18 commit ce38da7

File tree

60 files changed

+1307
-378
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1307
-378
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- run:
4444
name: Install eslint@6
4545
command: |
46-
npm install -D eslint@6.0.0
46+
npm install -D eslint@6.2.0
4747
- run:
4848
name: Install dependencies
4949
command: npm install

.eslintrc.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ module.exports = {
135135
{
136136
pattern: `https://eslint.vuejs.org/rules/{{name}}.html`
137137
}
138-
]
138+
],
139+
140+
'eslint-plugin/fixer-return': 'off'
139141
}
140142
}
141143
]

docs/.vuepress/components/eslint-code-block.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default {
8989
rules: this.rules,
9090
parser: 'vue-eslint-parser',
9191
parserOptions: {
92-
ecmaVersion: 2019,
92+
ecmaVersion: 2020,
9393
sourceType: 'module',
9494
ecmaFeatures: {
9595
jsx: true

docs/rules/valid-v-bind-sync.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ This rule checks whether every `.sync` modifier on `v-bind` directives is valid.
1515

1616
This rule reports `.sync` modifier on `v-bind` directives in the following cases:
1717

18-
- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`
18+
- 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" />`
19+
- The `.sync` modifier has potential null object property access. E.g. `<MyComponent v-bind:aaa.sync="(a?.b).c" />`
1920
- The `.sync` modifier is on non Vue-components. E.g. `<input v-bind:aaa.sync="foo"></div>`
2021
- The `.sync` modifier's reference is iteration variables. E.g. `<div v-for="x in list"><MyComponent v-bind:aaa.sync="x" /></div>`
2122

@@ -36,6 +37,9 @@ This rule reports `.sync` modifier on `v-bind` directives in the following cases
3637
<MyComponent v-bind:aaa.sync="foo + bar" />
3738
<MyComponent :aaa.sync="foo + bar" />
3839
40+
<MyComponent :aaa.sync="a?.b.c" />
41+
<MyComponent :aaa.sync="(a?.b).c" />
42+
3943
<input v-bind:aaa.sync="foo">
4044
<input :aaa.sync="foo">
4145

docs/rules/valid-v-model.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ This rule reports `v-model` directives in the following cases:
1818
- The directive used on HTMLElement has an argument. E.g. `<input v-model:aaa="foo">`
1919
- The directive used on HTMLElement has modifiers which are not supported. E.g. `<input v-model.bbb="foo">`
2020
- The directive does not have that attribute value. E.g. `<input v-model>`
21-
- The directive does not have the attribute value which is valid as LHS. E.g. `<input v-model="foo() + bar()">`
21+
- 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">`
22+
- The directive has potential null object property access. E.g. `<input v-model="(a?.b).c">`
2223
- The directive is on unsupported elements. E.g. `<div v-model="foo"></div>`
2324
- The directive is on `<input>` elements which their types are `file`. E.g. `<input type="file" v-model="foo">`
2425
- The directive's reference is iteration variables. E.g. `<div v-for="x in list"><input type="file" v-model="x"></div>`
@@ -44,6 +45,8 @@ This rule reports `v-model` directives in the following cases:
4445
<input v-model:aaa="foo">
4546
<input v-model.bbb="foo">
4647
<input v-model="foo + bar">
48+
<input v-model="a?.b.c">
49+
<input v-model="(a?.b).c">
4750
<div v-model="foo"/>
4851
<div v-for="todo in todos">
4952
<input v-model="todo">

docs/user-guide/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ yarn add -D eslint eslint-plugin-vue@next
1818
```
1919

2020
::: tip Requirements
21-
- ESLint v6.0.0 and above
21+
- ESLint v6.2.0 and above
2222
- Node.js v8.10.0 and above
2323
:::
2424

lib/configs/base.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
module.exports = {
77
parser: require.resolve('vue-eslint-parser'),
88
parserOptions: {
9-
ecmaVersion: 2018,
9+
ecmaVersion: 2020,
1010
sourceType: 'module'
1111
},
1212
env: {

lib/rules/component-name-in-template-casing.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,13 @@ module.exports = {
135135
name,
136136
caseType
137137
},
138-
fix: (fixer) => {
138+
*fix(fixer) {
139+
yield fixer.replaceText(open, `<${casingName}`)
139140
const endTag = node.endTag
140-
if (!endTag) {
141-
return fixer.replaceText(open, `<${casingName}`)
141+
if (endTag) {
142+
const endTagOpen = tokens.getFirstToken(endTag)
143+
yield fixer.replaceText(endTagOpen, `</${casingName}`)
142144
}
143-
const endTagOpen = tokens.getFirstToken(endTag)
144-
return [
145-
fixer.replaceText(open, `<${casingName}`),
146-
fixer.replaceText(endTagOpen, `</${casingName}`)
147-
]
148145
}
149146
})
150147
}

lib/rules/custom-event-name-casing.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function getNameParamNode(node) {
4848
* @param {CallExpression} node CallExpression
4949
*/
5050
function getCalleeMemberNode(node) {
51-
const callee = node.callee
51+
const callee = utils.skipChainExpression(node.callee)
5252

5353
if (callee.type === 'MemberExpression') {
5454
const name = utils.getStaticPropertyName(callee)
@@ -116,7 +116,7 @@ module.exports = {
116116
utils.compositingVisitors(
117117
utils.defineVueVisitor(context, {
118118
onSetupFunctionEnter(node, { node: vueNode }) {
119-
const contextParam = utils.unwrapAssignmentPattern(node.params[1])
119+
const contextParam = utils.skipDefaultParamValue(node.params[1])
120120
if (!contextParam) {
121121
// no arguments
122122
return

lib/rules/html-self-closing.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ module.exports = {
163163
elementType: ELEMENT_TYPE_MESSAGES[elementType],
164164
name: node.rawName
165165
},
166-
fix: (fixer) => {
166+
fix(fixer) {
167167
const tokens = context.parserServices.getTemplateBodyTokenStore()
168168
const close = tokens.getLastToken(node.startTag)
169169
if (close.type !== 'HTMLTagClose') {
@@ -187,7 +187,7 @@ module.exports = {
187187
elementType: ELEMENT_TYPE_MESSAGES[elementType],
188188
name: node.rawName
189189
},
190-
fix: (fixer) => {
190+
fix(fixer) {
191191
const tokens = context.parserServices.getTemplateBodyTokenStore()
192192
const close = tokens.getLastToken(node.startTag)
193193
if (close.type !== 'HTMLSelfClosingTagClose') {

lib/rules/no-async-in-computed-properties.js

+16-17
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ const TIMED_FUNCTIONS = [
2626
* @param {CallExpression} node
2727
*/
2828
function isTimedFunction(node) {
29+
const callee = utils.skipChainExpression(node.callee)
2930
return (
3031
((node.type === 'CallExpression' &&
31-
node.callee.type === 'Identifier' &&
32-
TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1) ||
32+
callee.type === 'Identifier' &&
33+
TIMED_FUNCTIONS.indexOf(callee.name) !== -1) ||
3334
(node.type === 'CallExpression' &&
34-
node.callee.type === 'MemberExpression' &&
35-
node.callee.object.type === 'Identifier' &&
36-
node.callee.object.name === 'window' &&
37-
node.callee.property.type === 'Identifier' &&
38-
TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1)) &&
35+
callee.type === 'MemberExpression' &&
36+
callee.object.type === 'Identifier' &&
37+
callee.object.name === 'window' &&
38+
callee.property.type === 'Identifier' &&
39+
TIMED_FUNCTIONS.indexOf(callee.property.name) !== -1)) &&
3940
node.arguments.length
4041
)
4142
}
@@ -44,18 +45,16 @@ function isTimedFunction(node) {
4445
* @param {CallExpression} node
4546
*/
4647
function isPromise(node) {
47-
if (
48-
node.type === 'CallExpression' &&
49-
node.callee.type === 'MemberExpression'
50-
) {
48+
const callee = utils.skipChainExpression(node.callee)
49+
if (node.type === 'CallExpression' && callee.type === 'MemberExpression') {
5150
return (
5251
// hello.PROMISE_FUNCTION()
53-
(node.callee.property.type === 'Identifier' &&
54-
PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
55-
(node.callee.object.type === 'Identifier' &&
56-
node.callee.object.name === 'Promise' &&
57-
node.callee.property.type === 'Identifier' &&
58-
PROMISE_METHODS.indexOf(node.callee.property.name) !== -1)
52+
(callee.property.type === 'Identifier' &&
53+
PROMISE_FUNCTIONS.indexOf(callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
54+
(callee.object.type === 'Identifier' &&
55+
callee.object.name === 'Promise' &&
56+
callee.property.type === 'Identifier' &&
57+
PROMISE_METHODS.indexOf(callee.property.name) !== -1)
5958
)
6059
}
6160
return false

lib/rules/no-deprecated-events-api.js

+19-6
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,26 @@ module.exports = {
3232
/** @param {RuleContext} context */
3333
create(context) {
3434
return utils.defineVueVisitor(context, {
35-
/** @param {MemberExpression & {parent: CallExpression}} node */
36-
'CallExpression > MemberExpression'(node) {
37-
const call = node.parent
35+
/** @param {MemberExpression & ({parent: CallExpression} | {parent: ChainExpression & {parent: CallExpression}})} node */
36+
'CallExpression > MemberExpression, CallExpression > ChainExpression > MemberExpression'(
37+
node
38+
) {
39+
const call =
40+
node.parent.type === 'ChainExpression'
41+
? node.parent.parent
42+
: node.parent
43+
44+
if (call.optional) {
45+
// It is OK because checking whether it is deprecated.
46+
// e.g. `this.$on?.()`
47+
return
48+
}
49+
3850
if (
39-
call.callee !== node ||
40-
node.property.type !== 'Identifier' ||
41-
!['$on', '$off', '$once'].includes(node.property.name)
51+
utils.skipChainExpression(call.callee) !== node ||
52+
!['$on', '$off', '$once'].includes(
53+
utils.getStaticPropertyName(node) || ''
54+
)
4255
) {
4356
return
4457
}

lib/rules/no-deprecated-v-bind-sync.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module.exports = {
3939
node,
4040
loc: node.loc,
4141
messageId: 'syncModifierIsDeprecated',
42-
fix: (fixer) => {
42+
fix(fixer) {
4343
if (node.key.argument == null) {
4444
// is using spread syntax
4545
return null

lib/rules/no-deprecated-v-on-number-modifiers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ module.exports = {
4747
context.report({
4848
node: modifier,
4949
messageId: 'numberModifierIsDeprecated',
50-
fix: (fixer) => {
50+
fix(fixer) {
5151
const key = keyCodeToKey[keyCodes]
5252
if (!key) return null
5353

lib/rules/no-deprecated-vue-config-keycodes.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
'use strict'
66

7+
const utils = require('../utils')
8+
79
// ------------------------------------------------------------------------------
810
// Rule Definition
911
// ------------------------------------------------------------------------------
@@ -31,7 +33,7 @@ module.exports = {
3133
"MemberExpression[property.type='Identifier'][property.name='keyCodes']"(
3234
node
3335
) {
34-
const config = node.object
36+
const config = utils.skipChainExpression(node.object)
3537
if (
3638
config.type !== 'MemberExpression' ||
3739
config.property.type !== 'Identifier' ||

lib/rules/no-multiple-slot-args.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,12 @@ module.exports = {
100100
return utils.defineVueVisitor(context, {
101101
/** @param {MemberExpression} node */
102102
MemberExpression(node) {
103-
const object = node.object
103+
const object = utils.skipChainExpression(node.object)
104104
if (object.type !== 'MemberExpression') {
105105
return
106106
}
107-
if (
108-
object.property.type !== 'Identifier' ||
109-
(object.property.name !== '$slots' &&
110-
object.property.name !== '$scopedSlots')
111-
) {
107+
const name = utils.getStaticPropertyName(object)
108+
if (!name || (name !== '$slots' && name !== '$scopedSlots')) {
112109
return
113110
}
114111
if (!utils.isThis(object.object, context)) {

lib/rules/no-setup-props-destructure.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,18 @@ module.exports = {
4949
return
5050
}
5151

52+
const rightNode = utils.skipChainExpression(right)
5253
if (
5354
left.type !== 'ArrayPattern' &&
5455
left.type !== 'ObjectPattern' &&
55-
right.type !== 'MemberExpression'
56+
rightNode.type !== 'MemberExpression'
5657
) {
5758
return
5859
}
59-
6060
/** @type {Expression | Super} */
61-
let rightId = right
61+
let rightId = rightNode
6262
while (rightId.type === 'MemberExpression') {
63-
rightId = rightId.object
63+
rightId = utils.skipChainExpression(rightId.object)
6464
}
6565
if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) {
6666
report(left, 'getProperty')
@@ -84,7 +84,7 @@ module.exports = {
8484
}
8585
},
8686
onSetupFunctionEnter(node) {
87-
const propsParam = utils.unwrapAssignmentPattern(node.params[0])
87+
const propsParam = utils.skipDefaultParamValue(node.params[0])
8888
if (!propsParam) {
8989
// no arguments
9090
return

lib/rules/no-unused-properties.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function getObjectPatternPropertyPatternTracker(pattern) {
221221
}
222222

223223
/**
224-
* @param {Identifier | MemberExpression | ThisExpression} node
224+
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
225225
* @param {RuleContext} context
226226
* @returns {UsedProps}
227227
*/
@@ -304,6 +304,14 @@ function extractPatternOrThisProperties(node, context) {
304304
}
305305
}
306306
}
307+
} else if (parent.type === 'ChainExpression') {
308+
const { usedNames, unknown, calls } = extractPatternOrThisProperties(
309+
parent,
310+
context
311+
)
312+
result.usedNames.addAll(usedNames)
313+
result.unknown = result.unknown || unknown
314+
result.calls.push(...calls)
307315
}
308316
return result
309317
}

lib/rules/no-useless-mustaches.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ module.exports = {
142142
return null
143143
}
144144

145-
return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))]
145+
return fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))
146146
}
147147
})
148148
}

lib/rules/no-useless-v-bind.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ module.exports = {
111111
context.report({
112112
node,
113113
messageId: 'unexpected',
114-
fix(fixer) {
114+
*fix(fixer) {
115115
if (hasComment || hasEscape) {
116116
// cannot fix
117-
return null
117+
return
118118
}
119119
const text = sourceCode.getText(value)
120120
const quoteChar = text[0]
@@ -126,6 +126,8 @@ module.exports = {
126126
node.key.name.range[1] + (shorthand ? 0 : 1)
127127
]
128128

129+
yield fixer.removeRange(keyDirectiveRange)
130+
129131
let attrValue
130132
if (quoteChar === '"') {
131133
attrValue = strValue.replace(DOUBLE_QUOTES_RE, '&quot;')
@@ -136,10 +138,7 @@ module.exports = {
136138
.replace(DOUBLE_QUOTES_RE, '&quot;')
137139
.replace(SINGLE_QUOTES_RE, '&apos;')
138140
}
139-
return [
140-
fixer.removeRange(keyDirectiveRange),
141-
fixer.replaceText(expression, attrValue)
142-
]
141+
yield fixer.replaceText(expression, attrValue)
143142
}
144143
})
145144
}

0 commit comments

Comments
 (0)