Skip to content

Commit 31a38a9

Browse files
dsl101David Lomasota-meshi
authored
Add option to show warnings when props could be accessed via this (#2101)
Co-authored-by: David Lomas <[email protected]> Co-authored-by: Yosuke Ota <[email protected]>
1 parent 6c32bf5 commit 31a38a9

File tree

4 files changed

+270
-10
lines changed

4 files changed

+270
-10
lines changed

docs/rules/no-unused-properties.md

+69-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ since: v7.0.0
1414
This rule is aimed at eliminating unused properties.
1515

1616
::: warning Note
17-
This rule cannot be checked for use in other components (e.g. `mixins`, Property access via `$refs`) and use in places where the scope cannot be determined.
17+
This rule cannot check for use of properties by other components (e.g. `mixins`, property access via `$refs`) and use in places where the scope cannot be determined. Some access to properties might be implied, for example accessing data or computed via a variable such as `this[varName]`. In this case, the default is to assume all properties, methods, etc. are 'used'. See the `unreferencedOptions` for a more strict interpretation of 'use' in these cases.
1818
:::
1919

2020
<eslint-code-block :rules="{'vue/no-unused-properties': ['error']}">
@@ -56,7 +56,8 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
5656
"vue/no-unused-properties": ["error", {
5757
"groups": ["props"],
5858
"deepData": false,
59-
"ignorePublicMembers": false
59+
"ignorePublicMembers": false,
60+
"unreferencedOptions": []
6061
}]
6162
}
6263
```
@@ -69,6 +70,7 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
6970
- `"setup"`
7071
- `deepData` (`boolean`) If `true`, the object of the property defined in `data` will be searched deeply. Default is `false`. Include `"data"` in `groups` to use this option.
7172
- `ignorePublicMembers` (`boolean`) If `true`, members marked with a [JSDoc `/** @public */` tag](https://jsdoc.app/tags-public.html) will be ignored. Default is `false`.
73+
- `unreferencedOptions` (`string[]`) Array of access methods that should be interpreted as leaving properties unreferenced. Currently, two such methods are available: `unknownMemberAsUnreferenced`, and `returnAsUnreferenced`. See examples below.
7274

7375
### `"groups": ["props", "data"]`
7476

@@ -218,6 +220,71 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
218220

219221
</eslint-code-block>
220222

223+
### `{ "groups": ["computed"], "unreferencedOptions": ["unknownMemberAsUnreferenced"] }`
224+
225+
<eslint-code-block :rules="{'vue/no-unused-properties': ['error', {groups: ['computed'], unreferencedOptions: ['unknownMemberAsUnreferenced']}]}">
226+
227+
```vue
228+
<template>
229+
230+
</template>
231+
<script>
232+
export default {
233+
computed: {
234+
one () {
235+
return 1
236+
},
237+
two () {
238+
return 2
239+
}
240+
},
241+
methods: {
242+
handler () {
243+
/* ✓ GOOD - explicit access to computed */
244+
const a = this.one
245+
const i = 'two'
246+
/* ✗ BAD - unknown access via a variable, two will be reported as unreferenced */
247+
return this[i]
248+
},
249+
}
250+
}
251+
</script>
252+
```
253+
254+
</eslint-code-block>
255+
256+
### `{ "groups": ["computed"], "unreferencedOptions": ["returnAsUnreferenced"] }`
257+
258+
<eslint-code-block :rules="{'vue/no-unused-properties': ['error', {groups: ['computed'], unreferencedOptions: ['returnAsUnreferenced']}]}">
259+
260+
```vue
261+
<template>
262+
263+
</template>
264+
<script>
265+
export default {
266+
computed: {
267+
one () {
268+
return 1
269+
},
270+
two () {
271+
return 2
272+
}
273+
},
274+
methods: {
275+
handler () {
276+
/* ✓ GOOD - explicit access to computed */
277+
const a = this.one
278+
/* ✗ BAD - any property could be accessed by returning `this`, but two will still be reported as unreferenced */
279+
return this
280+
},
281+
}
282+
}
283+
</script>
284+
```
285+
286+
</eslint-code-block>
287+
221288
## :rocket: Version
222289

223290
This rule was introduced in eslint-plugin-vue v7.0.0

lib/rules/no-unused-properties.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const GROUP_SETUP = 'setup'
5555
const GROUP_WATCHER = 'watch'
5656
const GROUP_EXPOSE = 'expose'
5757

58+
const UNREFERENCED_UNKNOWN_MEMBER = 'unknownMemberAsUnreferenced'
59+
const UNREFERENCED_RETURN = 'returnAsUnreferenced'
60+
5861
const PROPERTY_LABEL = {
5962
props: 'property',
6063
data: 'data',
@@ -206,7 +209,15 @@ module.exports = {
206209
uniqueItems: true
207210
},
208211
deepData: { type: 'boolean' },
209-
ignorePublicMembers: { type: 'boolean' }
212+
ignorePublicMembers: { type: 'boolean' },
213+
unreferencedOptions: {
214+
type: 'array',
215+
items: {
216+
enum: [UNREFERENCED_UNKNOWN_MEMBER, UNREFERENCED_RETURN]
217+
},
218+
additionalItems: false,
219+
uniqueItems: true
220+
}
210221
},
211222
additionalProperties: false
212223
}
@@ -221,8 +232,17 @@ module.exports = {
221232
const groups = new Set(options.groups || [GROUP_PROPERTY])
222233
const deepData = Boolean(options.deepData)
223234
const ignorePublicMembers = Boolean(options.ignorePublicMembers)
235+
const unreferencedOptions = new Set(options.unreferencedOptions || [])
224236

225-
const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
237+
const propertyReferenceExtractor = definePropertyReferenceExtractor(
238+
context,
239+
{
240+
unknownMemberAsUnreferenced: unreferencedOptions.has(
241+
UNREFERENCED_UNKNOWN_MEMBER
242+
),
243+
returnAsUnreferenced: unreferencedOptions.has(UNREFERENCED_RETURN)
244+
}
245+
)
226246

227247
/** @type {TemplatePropertiesContainer} */
228248
const templatePropertiesContainer = {

lib/utils/property-references.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ module.exports = {
9292
/**
9393
* @param {RuleContext} context The rule context.
9494
*/
95-
function definePropertyReferenceExtractor(context) {
95+
function definePropertyReferenceExtractor(
96+
context,
97+
{ unknownMemberAsUnreferenced = false, returnAsUnreferenced = false } = {}
98+
) {
9699
/** @type {Map<Expression, IPropertyReferences>} */
97100
const cacheForExpression = new Map()
98101
/** @type {Map<Pattern, IPropertyReferences>} */
@@ -314,9 +317,15 @@ function definePropertyReferenceExtractor(context) {
314317
if (parent.object === node) {
315318
// `arg.foo`
316319
const name = utils.getStaticPropertyName(parent)
317-
return name
318-
? new PropertyReferencesForMember(parent, name, withInTemplate)
319-
: ANY
320+
if (name) {
321+
return new PropertyReferencesForMember(
322+
parent,
323+
name,
324+
withInTemplate
325+
)
326+
} else {
327+
return unknownMemberAsUnreferenced ? NEVER : ANY
328+
}
320329
}
321330
return NEVER
322331
}
@@ -331,12 +340,18 @@ function definePropertyReferenceExtractor(context) {
331340
return extractFromExpression(parent, withInTemplate)
332341
}
333342
case 'ArrowFunctionExpression':
334-
case 'ReturnStatement':
335343
case 'VExpressionContainer':
336344
case 'Property':
337345
case 'ArrayExpression': {
338346
return maybeExternalUsed(parent) ? ANY : NEVER
339347
}
348+
case 'ReturnStatement': {
349+
if (returnAsUnreferenced) {
350+
return NEVER
351+
} else {
352+
return maybeExternalUsed(parent) ? ANY : NEVER
353+
}
354+
}
340355
}
341356
return NEVER
342357
}

tests/lib/rules/no-unused-properties.js

+159-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ const allOptions = [
2121
]
2222
const deepDataOptions = [{ groups: ['data'], deepData: true }]
2323

24+
const unreferencedOptions = {
25+
// Report errors when accessing via unknown property, e.g. this[varName]
26+
unknownMemberAsUnreferenced: [
27+
{
28+
groups: ['computed'],
29+
unreferencedOptions: ['unknownMemberAsUnreferenced']
30+
}
31+
],
32+
// Report errors when returning this
33+
returnAsUnreferenced: [
34+
{
35+
groups: ['computed'],
36+
unreferencedOptions: ['returnAsUnreferenced']
37+
}
38+
],
39+
// Report all
40+
all: [
41+
{
42+
groups: ['computed'],
43+
unreferencedOptions: [
44+
'unknownMemberAsUnreferenced',
45+
'returnAsUnreferenced'
46+
]
47+
}
48+
]
49+
}
50+
2451
tester.run('no-unused-properties', rule, {
2552
valid: [
2653
// a property used in a script expression
@@ -1699,7 +1726,6 @@ tester.run('no-unused-properties', rule, {
16991726
</script>`
17001727
}
17011728
],
1702-
17031729
invalid: [
17041730
// unused property
17051731
{
@@ -2803,6 +2829,138 @@ tester.run('no-unused-properties', rule, {
28032829
line: 10
28042830
}
28052831
]
2832+
},
2833+
2834+
// unreferencedOptions: unknownMemberAsUnreferenced
2835+
{
2836+
filename: 'test.vue',
2837+
code: `
2838+
<script>
2839+
export default {
2840+
computed: {
2841+
one () {
2842+
return 1
2843+
},
2844+
two () {
2845+
return 2
2846+
}
2847+
},
2848+
methods: {
2849+
handler () {
2850+
const a = this.one
2851+
const i = 'two'
2852+
return this[i]
2853+
},
2854+
}
2855+
}
2856+
</script>`,
2857+
options: unreferencedOptions.unknownMemberAsUnreferenced,
2858+
errors: [
2859+
{
2860+
message: "'two' of computed property found, but never used.",
2861+
line: 8
2862+
}
2863+
]
2864+
},
2865+
// unreferencedOptions: returnAsUnreferenced
2866+
{
2867+
filename: 'test.vue',
2868+
code: `
2869+
<script>
2870+
export default {
2871+
computed: {
2872+
one () {
2873+
return 1
2874+
},
2875+
two () {
2876+
return 2
2877+
}
2878+
},
2879+
methods: {
2880+
handler () {
2881+
const a = this.one
2882+
return this
2883+
},
2884+
}
2885+
}
2886+
</script>`,
2887+
options: unreferencedOptions.returnAsUnreferenced,
2888+
errors: [
2889+
{
2890+
message: "'two' of computed property found, but never used.",
2891+
line: 8
2892+
}
2893+
]
2894+
},
2895+
// unreferencedOptions: returnAsUnreferenced via variable with deepData
2896+
{
2897+
filename: 'test.vue',
2898+
code: `
2899+
<script>
2900+
export default {
2901+
data () {
2902+
return {
2903+
foo: {
2904+
bar: 1
2905+
},
2906+
baz: 2
2907+
}
2908+
},
2909+
methods: {
2910+
handler () {
2911+
const vm = this
2912+
console.log(vm.baz)
2913+
return vm.foo
2914+
},
2915+
}
2916+
}
2917+
</script>
2918+
`,
2919+
options: [
2920+
{
2921+
groups: ['data'],
2922+
unreferencedOptions: ['returnAsUnreferenced'],
2923+
deepData: true
2924+
}
2925+
],
2926+
errors: [
2927+
{
2928+
message: "'foo.bar' of data found, but never used.",
2929+
line: 7
2930+
}
2931+
]
2932+
},
2933+
// unreferencedOptions: all
2934+
{
2935+
filename: 'test.vue',
2936+
code: `
2937+
<script>
2938+
export default {
2939+
computed: {
2940+
one () {
2941+
return 1
2942+
},
2943+
two () {
2944+
return 2
2945+
}
2946+
},
2947+
methods: {
2948+
handler () {
2949+
const a = this.one
2950+
const i = 'two'
2951+
const b = this[i]
2952+
return this
2953+
},
2954+
}
2955+
}
2956+
</script>`,
2957+
options: unreferencedOptions.all,
2958+
errors: [
2959+
{
2960+
message: "'two' of computed property found, but never used.",
2961+
line: 8
2962+
}
2963+
]
28062964
}
28072965
]
28082966
})

0 commit comments

Comments
 (0)