Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 80f86cf

Browse files
committedAug 8, 2022
feat: add optional-props-using-with-defaults
1 parent f358817 commit 80f86cf

File tree

3 files changed

+810
-0
lines changed

3 files changed

+810
-0
lines changed
 
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/optional-props-using-with-defaults
5+
description: 'enforce props with default values ​​to be optional',
6+
---
7+
# vue/optional-props-using-with-defaults
8+
9+
> enforce props with default values ​​to be optional
10+
11+
## :book: Rule Details
12+
13+
This rule enforce props with default values ​​to be optional.
14+
Because when a required prop declared with a default value, but it doesn't be passed value when using it, it will be assigned the default value. So a required prop with default value is same as a optional prop.
15+
16+
<eslint-code-block :rules="{'vue/optional-props-using-with-defaults': ['error']}">
17+
18+
```vue
19+
<script setup lang="ts">
20+
/* ✗ GOOD */
21+
const props = withDefaults(
22+
defineProps<{
23+
name?: string | number
24+
age?: number
25+
}>(),
26+
{
27+
name: "Foo",
28+
}
29+
);
30+
31+
/* ✗ BAD */
32+
const props = withDefaults(
33+
defineProps<{
34+
name: string | number
35+
age?: number
36+
}>(),
37+
{
38+
name: "Foo",
39+
}
40+
);
41+
</script>
42+
```
43+
44+
</eslint-code-block>
45+
46+
## :wrench: Options
47+
48+
Nothing.
49+
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @author @neferqiqi
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
// ------------------------------------------------------------------------------
7+
// Requirements
8+
// ------------------------------------------------------------------------------
9+
10+
const utils = require('../utils')
11+
/**
12+
* @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
13+
*/
14+
15+
// ------------------------------------------------------------------------------
16+
// Helpers
17+
// ------------------------------------------------------------------------------
18+
19+
// ...
20+
21+
// ------------------------------------------------------------------------------
22+
// Rule Definition
23+
// ------------------------------------------------------------------------------
24+
25+
module.exports = {
26+
meta: {
27+
type: 'suggestion',
28+
docs: {
29+
description: 'enforce props with default values ​​to be optional',
30+
categories: undefined,
31+
url: 'https://eslint.vuejs.org/rules/optional-props-using-with-defaults.html'
32+
},
33+
fixable: 'code',
34+
schema: [],
35+
messages: {
36+
// ...
37+
}
38+
},
39+
/** @param {RuleContext} context */
40+
create(context) {
41+
/**
42+
* @param {ComponentTypeProp} prop
43+
* @param {Token[]} tokens
44+
* */
45+
const findKeyToken = (prop, tokens) =>
46+
tokens.find((token) => {
47+
const isKeyIdentifierEqual =
48+
prop.key.type === 'Identifier' && token.value === prop.key.name
49+
const isKeyLiteralEqual =
50+
prop.key.type === 'Literal' && token.value === prop.key.raw
51+
return isKeyIdentifierEqual || isKeyLiteralEqual
52+
})
53+
54+
return utils.defineScriptSetupVisitor(context, {
55+
onDefinePropsEnter(node, props) {
56+
if (!utils.hasWithDefaults(node)) {
57+
return
58+
}
59+
const withDefaultsProps = Object.keys(
60+
utils.getWithDefaultsPropExpressions(node)
61+
)
62+
const requiredProps = props.flatMap((item) =>
63+
item.type === 'type' && item.required ? [item] : []
64+
)
65+
66+
for (const prop of requiredProps) {
67+
if (withDefaultsProps.includes(prop.propName)) {
68+
const firstToken = context.getSourceCode().getFirstToken(prop.node)
69+
// skip setter & getter case
70+
if (firstToken.value === 'get' || firstToken.value === 'set') {
71+
return
72+
}
73+
// skip computed
74+
if (prop.node.computed) {
75+
return
76+
}
77+
const keyToken = findKeyToken(
78+
prop,
79+
context.getSourceCode().getTokens(prop.node)
80+
)
81+
if (!keyToken) return
82+
context.report({
83+
node: prop.node,
84+
loc: prop.node.loc,
85+
data: {
86+
key: prop.propName
87+
},
88+
message: `Prop "{{ key }}" should be optional.`,
89+
fix: (fixer) => fixer.insertTextAfter(keyToken, '?')
90+
})
91+
}
92+
}
93+
}
94+
})
95+
}
96+
}
Lines changed: 665 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,665 @@
1+
/**
2+
* @author neferqiqi
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../lib/rules/optional-props-using-with-defaults')
9+
10+
const tester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: {
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('optional-props-using-with-defaults', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: `
23+
<script setup lang="ts">
24+
interface TestPropType {
25+
name?: string
26+
age?: number
27+
}
28+
const props = withDefaults(
29+
defineProps<TestPropType>(),
30+
{
31+
name: "World",
32+
}
33+
);
34+
</script>
35+
`,
36+
parserOptions: {
37+
parser: require.resolve('@typescript-eslint/parser')
38+
}
39+
},
40+
{
41+
filename: 'test.vue',
42+
code: `
43+
<script setup lang="ts">
44+
type TestPropType = {
45+
name?: string
46+
age?: number
47+
}
48+
const props = withDefaults(
49+
defineProps<TestPropType>(),
50+
{
51+
name: "World",
52+
}
53+
);
54+
</script>
55+
`,
56+
parserOptions: {
57+
parser: require.resolve('@typescript-eslint/parser')
58+
}
59+
},
60+
{
61+
filename: 'test.vue',
62+
code: `
63+
<script setup lang="ts">
64+
interface TestPropType {
65+
name?
66+
}
67+
const props = withDefaults(
68+
defineProps<TestPropType>(),
69+
{
70+
name: "World",
71+
}
72+
);
73+
</script>
74+
`,
75+
parserOptions: {
76+
parser: require.resolve('@typescript-eslint/parser')
77+
}
78+
},
79+
{
80+
filename: 'test.vue',
81+
code: `
82+
<script setup lang="ts">
83+
interface TestPropType {
84+
get name(): string
85+
set name(a: string)
86+
age?: number
87+
}
88+
const props = withDefaults(
89+
defineProps<TestPropType>(),
90+
{
91+
'name': 'World',
92+
}
93+
);
94+
</script>
95+
`,
96+
parserOptions: {
97+
parser: require.resolve('@typescript-eslint/parser')
98+
}
99+
},
100+
{
101+
filename: 'test.vue',
102+
code: `
103+
<script setup lang="ts">
104+
interface TestPropType {
105+
get name(): void
106+
age?: number
107+
}
108+
const props = withDefaults(
109+
defineProps<TestPropType>(),
110+
{
111+
'name': 'World',
112+
}
113+
);
114+
</script>
115+
`,
116+
parserOptions: {
117+
parser: require.resolve('@typescript-eslint/parser')
118+
}
119+
},
120+
{
121+
filename: 'test.vue',
122+
code: `
123+
<script setup lang="ts">
124+
const [name] = 'test'
125+
126+
interface TestPropType {
127+
[name]: string
128+
age?: number
129+
}
130+
const props = withDefaults(
131+
defineProps<TestPropType>(),
132+
{
133+
[name]: 'World'
134+
}
135+
);
136+
</script>
137+
`,
138+
parserOptions: {
139+
parser: require.resolve('@typescript-eslint/parser')
140+
}
141+
}
142+
],
143+
invalid: [
144+
{
145+
filename: 'test.vue',
146+
code: `
147+
<script setup lang="ts">
148+
interface TestPropType {
149+
name: string
150+
age?: number
151+
}
152+
const props = withDefaults(
153+
defineProps<TestPropType>(),
154+
{
155+
name: "World",
156+
}
157+
);
158+
</script>
159+
`,
160+
output: `
161+
<script setup lang="ts">
162+
interface TestPropType {
163+
name?: string
164+
age?: number
165+
}
166+
const props = withDefaults(
167+
defineProps<TestPropType>(),
168+
{
169+
name: "World",
170+
}
171+
);
172+
</script>
173+
`,
174+
parserOptions: {
175+
parser: require.resolve('@typescript-eslint/parser')
176+
},
177+
errors: [
178+
{
179+
message: 'Prop "name" should be optional.',
180+
line: 4
181+
}
182+
]
183+
},
184+
{
185+
filename: 'test.vue',
186+
code: `
187+
<script setup lang="ts">
188+
interface TestPropType {
189+
name: string | number
190+
age?: number
191+
}
192+
const props = withDefaults(
193+
defineProps<TestPropType>(),
194+
{
195+
name: "World",
196+
}
197+
);
198+
</script>
199+
`,
200+
output: `
201+
<script setup lang="ts">
202+
interface TestPropType {
203+
name?: string | number
204+
age?: number
205+
}
206+
const props = withDefaults(
207+
defineProps<TestPropType>(),
208+
{
209+
name: "World",
210+
}
211+
);
212+
</script>
213+
`,
214+
parserOptions: {
215+
parser: require.resolve('@typescript-eslint/parser')
216+
},
217+
errors: [
218+
{
219+
message: 'Prop "name" should be optional.',
220+
line: 4
221+
}
222+
]
223+
},
224+
{
225+
filename: 'test.vue',
226+
code: `
227+
<script setup lang="ts">
228+
interface TestPropType {
229+
'na::me': string
230+
age?: number
231+
}
232+
const props = withDefaults(
233+
defineProps<TestPropType>(),
234+
{
235+
'na::me': "World",
236+
}
237+
);
238+
</script>
239+
`,
240+
output: `
241+
<script setup lang="ts">
242+
interface TestPropType {
243+
'na::me'?: string
244+
age?: number
245+
}
246+
const props = withDefaults(
247+
defineProps<TestPropType>(),
248+
{
249+
'na::me': "World",
250+
}
251+
);
252+
</script>
253+
`,
254+
parserOptions: {
255+
parser: require.resolve('@typescript-eslint/parser')
256+
},
257+
errors: [
258+
{
259+
message: 'Prop "na::me" should be optional.',
260+
line: 4
261+
}
262+
]
263+
},
264+
{
265+
filename: 'test.vue',
266+
code: `
267+
<script setup lang="ts">
268+
import nameType from 'name.ts';
269+
interface TestPropType {
270+
name: nameType
271+
age?: number
272+
}
273+
const props = withDefaults(
274+
defineProps<TestPropType>(),
275+
{
276+
name: "World",
277+
}
278+
);
279+
</script>
280+
`,
281+
output: `
282+
<script setup lang="ts">
283+
import nameType from 'name.ts';
284+
interface TestPropType {
285+
name?: nameType
286+
age?: number
287+
}
288+
const props = withDefaults(
289+
defineProps<TestPropType>(),
290+
{
291+
name: "World",
292+
}
293+
);
294+
</script>
295+
`,
296+
parserOptions: {
297+
parser: require.resolve('@typescript-eslint/parser')
298+
},
299+
errors: [
300+
{
301+
message: 'Prop "name" should be optional.',
302+
line: 5
303+
}
304+
]
305+
},
306+
{
307+
filename: 'test.vue',
308+
code: `
309+
<script setup lang="ts">
310+
interface TestPropType {
311+
name
312+
}
313+
const props = withDefaults(
314+
defineProps<TestPropType>(),
315+
{
316+
name: "World",
317+
}
318+
);
319+
</script>
320+
`,
321+
output: `
322+
<script setup lang="ts">
323+
interface TestPropType {
324+
name?
325+
}
326+
const props = withDefaults(
327+
defineProps<TestPropType>(),
328+
{
329+
name: "World",
330+
}
331+
);
332+
</script>
333+
`,
334+
parserOptions: {
335+
parser: require.resolve('@typescript-eslint/parser')
336+
},
337+
errors: [
338+
{
339+
message: 'Prop "name" should be optional.',
340+
line: 4
341+
}
342+
]
343+
},
344+
{
345+
filename: 'test.vue',
346+
code: `
347+
<script setup lang="ts">
348+
interface TestPropType {
349+
name
350+
age?: number
351+
}
352+
const props = withDefaults(
353+
defineProps<TestPropType>(),
354+
{
355+
name: "World",
356+
}
357+
);
358+
</script>
359+
`,
360+
output: `
361+
<script setup lang="ts">
362+
interface TestPropType {
363+
name?
364+
age?: number
365+
}
366+
const props = withDefaults(
367+
defineProps<TestPropType>(),
368+
{
369+
name: "World",
370+
}
371+
);
372+
</script>
373+
`,
374+
parserOptions: {
375+
parser: require.resolve('@typescript-eslint/parser')
376+
},
377+
errors: [
378+
{
379+
message: 'Prop "name" should be optional.',
380+
line: 4
381+
}
382+
]
383+
},
384+
{
385+
filename: 'test.vue',
386+
code: `
387+
<script setup lang="ts">
388+
interface TestPropType {
389+
'na\\"me2'
390+
age?: number
391+
}
392+
const props = withDefaults(
393+
defineProps<TestPropType>(),
394+
{
395+
'na\\"me2': "World",
396+
}
397+
);
398+
</script>
399+
`,
400+
output: `
401+
<script setup lang="ts">
402+
interface TestPropType {
403+
'na\\"me2'?
404+
age?: number
405+
}
406+
const props = withDefaults(
407+
defineProps<TestPropType>(),
408+
{
409+
'na\\"me2': "World",
410+
}
411+
);
412+
</script>
413+
`,
414+
parserOptions: {
415+
parser: require.resolve('@typescript-eslint/parser')
416+
},
417+
errors: [
418+
{
419+
message: 'Prop "na"me2" should be optional.',
420+
line: 4
421+
}
422+
]
423+
},
424+
{
425+
filename: 'test.vue',
426+
code: `
427+
<script setup lang="ts">
428+
interface TestPropType {
429+
foo(): void
430+
age?: number
431+
}
432+
const props = withDefaults(
433+
defineProps<TestPropType>(),
434+
{
435+
foo() {console.log(123)},
436+
}
437+
);
438+
</script>
439+
`,
440+
output: `
441+
<script setup lang="ts">
442+
interface TestPropType {
443+
foo?(): void
444+
age?: number
445+
}
446+
const props = withDefaults(
447+
defineProps<TestPropType>(),
448+
{
449+
foo() {console.log(123)},
450+
}
451+
);
452+
</script>
453+
`,
454+
parserOptions: {
455+
parser: require.resolve('@typescript-eslint/parser')
456+
},
457+
errors: [
458+
{
459+
message: 'Prop "foo" should be optional.',
460+
line: 4
461+
}
462+
]
463+
},
464+
{
465+
filename: 'test.vue',
466+
code: `
467+
<script setup lang="ts">
468+
interface TestPropType {
469+
readonly foo(): void
470+
age?: number
471+
}
472+
const props = withDefaults(
473+
defineProps<TestPropType>(),
474+
{
475+
foo() {console.log(123)},
476+
}
477+
);
478+
</script>
479+
`,
480+
output: `
481+
<script setup lang="ts">
482+
interface TestPropType {
483+
readonly foo?(): void
484+
age?: number
485+
}
486+
const props = withDefaults(
487+
defineProps<TestPropType>(),
488+
{
489+
foo() {console.log(123)},
490+
}
491+
);
492+
</script>
493+
`,
494+
parserOptions: {
495+
parser: require.resolve('@typescript-eslint/parser')
496+
},
497+
errors: [
498+
{
499+
message: 'Prop "foo" should be optional.',
500+
line: 4
501+
}
502+
]
503+
},
504+
{
505+
filename: 'test.vue',
506+
code: `
507+
<script setup lang="ts">
508+
interface TestPropType {
509+
readonly name
510+
age?: number
511+
}
512+
const props = withDefaults(
513+
defineProps<TestPropType>(),
514+
{
515+
name: 'World',
516+
}
517+
);
518+
</script>
519+
`,
520+
output: `
521+
<script setup lang="ts">
522+
interface TestPropType {
523+
readonly name?
524+
age?: number
525+
}
526+
const props = withDefaults(
527+
defineProps<TestPropType>(),
528+
{
529+
name: 'World',
530+
}
531+
);
532+
</script>
533+
`,
534+
parserOptions: {
535+
parser: require.resolve('@typescript-eslint/parser')
536+
},
537+
errors: [
538+
{
539+
message: 'Prop "name" should be optional.',
540+
line: 4
541+
}
542+
]
543+
},
544+
{
545+
filename: 'test.vue',
546+
code: `
547+
<script setup lang="ts">
548+
interface TestPropType {
549+
readonly 'name'
550+
age?: number
551+
}
552+
const props = withDefaults(
553+
defineProps<TestPropType>(),
554+
{
555+
'name': 'World',
556+
}
557+
);
558+
</script>
559+
`,
560+
output: `
561+
<script setup lang="ts">
562+
interface TestPropType {
563+
readonly 'name'?
564+
age?: number
565+
}
566+
const props = withDefaults(
567+
defineProps<TestPropType>(),
568+
{
569+
'name': 'World',
570+
}
571+
);
572+
</script>
573+
`,
574+
parserOptions: {
575+
parser: require.resolve('@typescript-eslint/parser')
576+
},
577+
errors: [
578+
{
579+
message: 'Prop "name" should be optional.',
580+
line: 4
581+
}
582+
]
583+
},
584+
{
585+
filename: 'test.vue',
586+
code: `
587+
<script setup lang="ts">
588+
interface TestPropType {
589+
readonly 'a'
590+
age?: number
591+
}
592+
const props = withDefaults(
593+
defineProps<TestPropType>(),
594+
{
595+
'\\u0061': 'World',
596+
}
597+
);
598+
</script>
599+
`,
600+
output: `
601+
<script setup lang="ts">
602+
interface TestPropType {
603+
readonly 'a'?
604+
age?: number
605+
}
606+
const props = withDefaults(
607+
defineProps<TestPropType>(),
608+
{
609+
'\\u0061': 'World',
610+
}
611+
);
612+
</script>
613+
`,
614+
parserOptions: {
615+
parser: require.resolve('@typescript-eslint/parser')
616+
},
617+
errors: [
618+
{
619+
message: 'Prop "a" should be optional.',
620+
line: 4
621+
}
622+
]
623+
},
624+
{
625+
filename: 'test.vue',
626+
code: `
627+
<script setup lang="ts">
628+
interface TestPropType {
629+
readonly '\\u0061'
630+
age?: number
631+
}
632+
const props = withDefaults(
633+
defineProps<TestPropType>(),
634+
{
635+
'a': 'World',
636+
}
637+
);
638+
</script>
639+
`,
640+
output: `
641+
<script setup lang="ts">
642+
interface TestPropType {
643+
readonly '\\u0061'?
644+
age?: number
645+
}
646+
const props = withDefaults(
647+
defineProps<TestPropType>(),
648+
{
649+
'a': 'World',
650+
}
651+
);
652+
</script>
653+
`,
654+
parserOptions: {
655+
parser: require.resolve('@typescript-eslint/parser')
656+
},
657+
errors: [
658+
{
659+
message: 'Prop "a" should be optional.',
660+
line: 4
661+
}
662+
]
663+
}
664+
]
665+
})

0 commit comments

Comments
 (0)
Please sign in to comment.