Skip to content

Commit 4499597

Browse files
committed
feat: autofix in define-props-declaration: runtime syntax to type-based syntax (vuejs#2465)
additional tests and refactoring
1 parent 583c0db commit 4499597

File tree

2 files changed

+206
-113
lines changed

2 files changed

+206
-113
lines changed

lib/rules/define-props-declaration.js

+127-112
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,127 @@ const mapNativeType = (/** @type {string} */ nativeType) => {
3636
}
3737
}
3838

39+
/**
40+
* @param {ComponentProp} prop
41+
* @param {SourceCode} sourceCode
42+
*/
43+
function getComponentPropData(prop, sourceCode) {
44+
const unknownType = {
45+
name: prop.propName,
46+
type: 'unknown',
47+
required: undefined,
48+
defaultValue: undefined
49+
}
50+
51+
if (prop.type !== 'object') {
52+
return unknownType
53+
}
54+
const type = optionGetType(prop.value, sourceCode)
55+
if (type === null) {
56+
return unknownType
57+
}
58+
const required = optionGetRequired(prop.value)
59+
const defaultValue = optionGetDefault(prop.value)
60+
61+
return {
62+
name: prop.propName,
63+
type: mapNativeType(type),
64+
required,
65+
defaultValue
66+
}
67+
}
68+
69+
/**
70+
* @param {Expression} node
71+
* @param {SourceCode} sourceCode
72+
* @returns {string | null}
73+
*/
74+
function optionGetType(node, sourceCode) {
75+
switch (node.type) {
76+
case 'Identifier': {
77+
return node.name
78+
}
79+
case 'ObjectExpression': {
80+
// foo: {
81+
const typeProperty = utils.findProperty(node, 'type')
82+
if (typeProperty == null) {
83+
return null
84+
}
85+
if (typeProperty.value.type === 'TSAsExpression') {
86+
const typeAnnotation = typeProperty.value.typeAnnotation
87+
if (typeAnnotation.typeName.name !== 'PropType') {
88+
return null
89+
}
90+
91+
// in some project configuration parser populates deprecated field `typeParameters` instead of `typeArguments`
92+
const typeArguments =
93+
'typeArguments' in typeProperty.value
94+
? typeAnnotation.typeArguments
95+
: typeAnnotation.typeParameters
96+
97+
const typeArgument = Array.isArray(typeArguments)
98+
? typeArguments[0].params[0]
99+
: typeArguments.params[0]
100+
101+
if (typeArgument === undefined) {
102+
return null
103+
}
104+
105+
return sourceCode.getText(typeArgument)
106+
}
107+
return optionGetType(typeProperty.value, sourceCode)
108+
}
109+
case 'ArrayExpression': {
110+
return null
111+
}
112+
case 'FunctionExpression':
113+
case 'ArrowFunctionExpression': {
114+
return null
115+
}
116+
}
117+
118+
// Unknown
119+
return null
120+
}
121+
122+
/**
123+
* @param {Expression} node
124+
* @returns {boolean | undefined }
125+
*/
126+
function optionGetRequired(node) {
127+
if (node.type === 'ObjectExpression') {
128+
const requiredProperty = utils.findProperty(node, 'required')
129+
if (requiredProperty == null) {
130+
return undefined
131+
}
132+
133+
if (requiredProperty.value.type === 'Literal') {
134+
return Boolean(requiredProperty.value.value)
135+
}
136+
}
137+
138+
// Unknown
139+
return undefined
140+
}
141+
142+
/**
143+
* @param {Expression} node
144+
* @returns {Expression | undefined }
145+
*/
146+
function optionGetDefault(node) {
147+
if (node.type === 'ObjectExpression') {
148+
const defaultProperty = utils.findProperty(node, 'default')
149+
if (defaultProperty == null) {
150+
return undefined
151+
}
152+
153+
return defaultProperty.value
154+
}
155+
156+
// Unknown
157+
return undefined
158+
}
159+
39160
/**
40161
* @typedef {import('../utils').ComponentProp} ComponentProp
41162
*/
@@ -72,93 +193,6 @@ module.exports = {
72193
create(context) {
73194
const sourceCode = context.getSourceCode()
74195

75-
/**
76-
* @param {Expression} node
77-
* @returns {string | null}
78-
*/
79-
function optionGetType(node) {
80-
switch (node.type) {
81-
case 'Identifier': {
82-
return node.name
83-
}
84-
case 'ObjectExpression': {
85-
// foo: {
86-
const typeProperty = utils.findProperty(node, 'type')
87-
if (typeProperty == null) {
88-
return null
89-
}
90-
if (typeProperty.value.type === 'TSAsExpression') {
91-
if (
92-
typeProperty.value.typeAnnotation.typeName.name !== 'PropType'
93-
) {
94-
return null
95-
}
96-
97-
const typeArgument =
98-
typeProperty.value.typeAnnotation.typeArguments.params[0]
99-
if (typeArgument === undefined) {
100-
return null
101-
}
102-
103-
return sourceCode.getText(typeArgument)
104-
}
105-
return optionGetType(typeProperty.value)
106-
}
107-
case 'ArrayExpression': {
108-
// foo: [
109-
return null
110-
// return node.elements.map((arrayElement) =>
111-
// optionGetType(arrayElement)
112-
// )
113-
}
114-
case 'FunctionExpression':
115-
case 'ArrowFunctionExpression': {
116-
return null
117-
}
118-
}
119-
120-
// Unknown
121-
return null
122-
}
123-
124-
/**
125-
* @param {Expression} node
126-
* @returns {boolean | undefined }
127-
*/
128-
function optionGetRequired(node) {
129-
if (node.type === 'ObjectExpression') {
130-
const requiredProperty = utils.findProperty(node, 'required')
131-
if (requiredProperty == null) {
132-
return undefined
133-
}
134-
135-
if (requiredProperty.value.type === 'Literal') {
136-
return Boolean(requiredProperty.value.value)
137-
}
138-
}
139-
140-
// Unknown
141-
return undefined
142-
}
143-
144-
/**
145-
* @param {Expression} node
146-
* @returns {Expression | undefined }
147-
*/
148-
function optionGetDefault(node) {
149-
if (node.type === 'ObjectExpression') {
150-
const defaultProperty = utils.findProperty(node, 'default')
151-
if (defaultProperty == null) {
152-
return undefined
153-
}
154-
155-
return defaultProperty.value
156-
}
157-
158-
// Unknown
159-
return undefined
160-
}
161-
162196
const scriptSetup = utils.getScriptSetupElement(context)
163197
if (!scriptSetup || !utils.hasAttribute(scriptSetup, 'lang', 'ts')) {
164198
return {}
@@ -176,31 +210,9 @@ module.exports = {
176210
node,
177211
messageId: 'hasArg',
178212
*fix(fixer) {
179-
const propTypes = props.map((prop) => {
180-
const unknownType = {
181-
name: prop.propName,
182-
type: 'unknown',
183-
required: undefined,
184-
defaultValue: undefined
185-
}
186-
187-
if (prop.type !== 'object') {
188-
return unknownType
189-
}
190-
const type = optionGetType(prop.value)
191-
if (type === null) {
192-
return unknownType
193-
}
194-
const required = optionGetRequired(prop.value)
195-
const defaultValue = optionGetDefault(prop.value)
196-
197-
return {
198-
name: prop.propName,
199-
type: mapNativeType(type),
200-
required,
201-
defaultValue
202-
}
203-
})
213+
const propTypes = props.map((prop) =>
214+
getComponentPropData(prop, sourceCode)
215+
)
204216

205217
const definePropsType = `{ ${propTypes
206218
.map(
@@ -209,8 +221,10 @@ module.exports = {
209221
)
210222
.join(', ')} }`
211223

224+
// remove defineProps function parameters
212225
yield fixer.replaceText(node.arguments[0], '')
213226

227+
// add type annotation
214228
if (separateInterface) {
215229
const variableDeclarationNode = node.parent.parent
216230
if (!variableDeclarationNode) return
@@ -227,6 +241,7 @@ module.exports = {
227241
)
228242
}
229243

244+
// add defaults if needed
230245
const defaults = propTypes.filter(
231246
({ defaultValue }) => defaultValue
232247
)

tests/lib/rules/define-props-declaration.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/define-props-declaration')
1010
const tester = new RuleTester({
1111
languageOptions: {
1212
parser: require('vue-eslint-parser'),
13-
ecmaVersion: 'latest',
13+
ecmaVersion: '2020',
1414
sourceType: 'module',
1515
parserOptions: {
1616
parser: require.resolve('@typescript-eslint/parser')
@@ -289,6 +289,28 @@ tester.run('define-props-declaration', rule, {
289289
}
290290
]
291291
},
292+
// Custom type
293+
{
294+
filename: 'test.vue',
295+
code: `
296+
<script setup lang="ts">
297+
const props = defineProps({
298+
kind: User
299+
})
300+
</script>
301+
`,
302+
output: `
303+
<script setup lang="ts">
304+
const props = defineProps<{ kind: User }>()
305+
</script>
306+
`,
307+
errors: [
308+
{
309+
message: 'Use type-based declaration instead of runtime declaration.',
310+
line: 3
311+
}
312+
]
313+
},
292314
// Native Type with PropType
293315
{
294316
filename: 'test.vue',
@@ -337,6 +359,62 @@ tester.run('define-props-declaration', rule, {
337359
}
338360
]
339361
},
362+
// Object with PropType with separate type
363+
{
364+
filename: 'test.vue',
365+
code: `
366+
<script setup lang="ts">
367+
interface Kind { id: number; name: string }
368+
369+
const props = defineProps({
370+
kind: {
371+
type: Object as PropType<Kind>,
372+
}
373+
})
374+
</script>
375+
`,
376+
output: `
377+
<script setup lang="ts">
378+
interface Kind { id: number; name: string }
379+
380+
const props = defineProps<{ kind: Kind }>()
381+
</script>
382+
`,
383+
errors: [
384+
{
385+
message: 'Use type-based declaration instead of runtime declaration.',
386+
line: 5
387+
}
388+
]
389+
},
390+
// Object with PropType with separate imported type
391+
{
392+
filename: 'test.vue',
393+
code: `
394+
<script setup lang="ts">
395+
import Kind from 'test'
396+
397+
const props = defineProps({
398+
kind: {
399+
type: Object as PropType<Kind>,
400+
}
401+
})
402+
</script>
403+
`,
404+
output: `
405+
<script setup lang="ts">
406+
import Kind from 'test'
407+
408+
const props = defineProps<{ kind: Kind }>()
409+
</script>
410+
`,
411+
errors: [
412+
{
413+
message: 'Use type-based declaration instead of runtime declaration.',
414+
line: 5
415+
}
416+
]
417+
},
340418
// Array with PropType
341419
{
342420
filename: 'test.vue',

0 commit comments

Comments
 (0)