Skip to content

Commit 7f906ea

Browse files
Add vue/require-typed-object-prop rule (#1983)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent cd32f03 commit 7f906ea

File tree

5 files changed

+823
-0
lines changed

5 files changed

+823
-0
lines changed

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ For example:
268268
| [vue/require-macro-variable-name](./require-macro-variable-name.md) | require a certain macro variable name | :bulb: | :hammer: |
269269
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: |
270270
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
271+
| [vue/require-typed-object-prop](./require-typed-object-prop.md) | enforce adding type declarations to object props | :bulb: | :hammer: |
271272
| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
272273
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
273274
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-typed-object-prop
5+
description: enforce adding type declarations to object props
6+
---
7+
# vue/require-typed-object-prop
8+
9+
> enforce adding type declarations to object props
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
13+
14+
## :book: Rule Details
15+
16+
Prevent missing type declarations for non-primitive object props in TypeScript projects.
17+
18+
<eslint-code-block :rules="{'vue/require-typed-object-prop': ['error']}">
19+
20+
```vue
21+
<script lang="ts">
22+
export default {
23+
props: {
24+
// ✗ BAD
25+
bad1: Object,
26+
bad2: { type: Array },
27+
28+
// ✓ GOOD
29+
good1: Object as PropType<Anything>,
30+
good2: { type: Array as PropType<Anything[]> },
31+
good3: [String, Boolean], // or any other primitive type
32+
}
33+
}
34+
</script>
35+
```
36+
37+
</eslint-code-block>
38+
39+
## :wrench: Options
40+
41+
Nothing.
42+
43+
## :mute: When Not To Use It
44+
45+
When you're not using TypeScript in the project.
46+
47+
## :mag: Implementation
48+
49+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-typed-object-prop.js)
50+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-typed-object-prop.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ module.exports = {
192192
'require-render-return': require('./rules/require-render-return'),
193193
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
194194
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
195+
'require-typed-object-prop': require('./rules/require-typed-object-prop'),
195196
'require-typed-ref': require('./rules/require-typed-ref'),
196197
'require-v-for-key': require('./rules/require-v-for-key'),
197198
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @author Przemysław Jan Beigert
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/**
10+
* @param {RuleContext} context
11+
* @param {Identifier} identifierNode
12+
*/
13+
const checkPropIdentifierType = (context, identifierNode) => {
14+
if (identifierNode.name === 'Object' || identifierNode.name === 'Array') {
15+
const arrayTypeSuggestion = identifierNode.name === 'Array' ? '[]' : ''
16+
context.report({
17+
node: identifierNode,
18+
messageId: 'expectedTypeAnnotation',
19+
suggest: [
20+
{
21+
messageId: 'addTypeAnnotation',
22+
data: { type: `any${arrayTypeSuggestion}` },
23+
fix(fixer) {
24+
return fixer.insertTextAfter(
25+
identifierNode,
26+
` as PropType<any${arrayTypeSuggestion}>`
27+
)
28+
}
29+
},
30+
{
31+
messageId: 'addTypeAnnotation',
32+
data: { type: `unknown${arrayTypeSuggestion}` },
33+
fix(fixer) {
34+
return fixer.insertTextAfter(
35+
identifierNode,
36+
` as PropType<unknown${arrayTypeSuggestion}>`
37+
)
38+
}
39+
}
40+
]
41+
})
42+
}
43+
}
44+
45+
/**
46+
* @param {RuleContext} context
47+
* @param {ArrayExpression} arrayNode
48+
*/
49+
const checkPropArrayType = (context, arrayNode) => {
50+
for (const elementNode of arrayNode.elements) {
51+
if (elementNode?.type === 'Identifier') {
52+
checkPropIdentifierType(context, elementNode)
53+
}
54+
}
55+
}
56+
57+
/**
58+
* @param {RuleContext} context
59+
* @param {ObjectExpression} objectNode
60+
*/
61+
const checkPropObjectType = (context, objectNode) => {
62+
const typeProperty = objectNode.properties.find(
63+
(prop) =>
64+
prop.type === 'Property' &&
65+
prop.key.type === 'Identifier' &&
66+
prop.key.name === 'type'
67+
)
68+
if (!typeProperty || typeProperty.type !== 'Property') {
69+
return
70+
}
71+
72+
if (typeProperty.value.type === 'Identifier') {
73+
// `foo: { type: String }`
74+
checkPropIdentifierType(context, typeProperty.value)
75+
} else if (typeProperty.value.type === 'ArrayExpression') {
76+
// `foo: { type: [String, Boolean] }`
77+
checkPropArrayType(context, typeProperty.value)
78+
}
79+
}
80+
81+
/**
82+
* @param {import('../utils').ComponentProp} prop
83+
* @param {RuleContext} context
84+
*/
85+
const checkProp = (prop, context) => {
86+
if (prop.type !== 'object') {
87+
return
88+
}
89+
90+
switch (prop.node.value.type) {
91+
case 'Identifier': {
92+
// e.g. `foo: String`
93+
checkPropIdentifierType(context, prop.node.value)
94+
break
95+
}
96+
case 'ArrayExpression': {
97+
// e.g. `foo: [String, Boolean]`
98+
checkPropArrayType(context, prop.node.value)
99+
break
100+
}
101+
case 'ObjectExpression': {
102+
// e.g. `foo: { type: … }`
103+
checkPropObjectType(context, prop.node.value)
104+
return
105+
}
106+
}
107+
}
108+
109+
//------------------------------------------------------------------------------
110+
// Rule Definition
111+
//------------------------------------------------------------------------------
112+
113+
module.exports = {
114+
meta: {
115+
type: 'suggestion',
116+
docs: {
117+
description: 'enforce adding type declarations to object props',
118+
categories: undefined,
119+
recommended: false,
120+
url: 'https://eslint.vuejs.org/rules/require-typed-object-prop.html'
121+
},
122+
fixable: null,
123+
schema: [],
124+
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- `context.report` with suggestion is not recognized in `checkPropIdentifierType`
125+
hasSuggestions: true,
126+
messages: {
127+
expectedTypeAnnotation: 'Expected type annotation on object prop.',
128+
addTypeAnnotation: 'Add `{{ type }}` type annotation.'
129+
}
130+
},
131+
/** @param {RuleContext} context */
132+
create(context) {
133+
return utils.compositingVisitors(
134+
utils.defineScriptSetupVisitor(context, {
135+
onDefinePropsEnter(_node, props) {
136+
for (const prop of props) {
137+
checkProp(prop, context)
138+
}
139+
}
140+
}),
141+
utils.executeOnVue(context, (obj) => {
142+
const props = utils.getComponentPropsFromOptions(obj)
143+
144+
for (const prop of props) {
145+
checkProp(prop, context)
146+
}
147+
})
148+
)
149+
}
150+
}

0 commit comments

Comments
 (0)