Skip to content

Commit 11b2077

Browse files
authored
Update vue/no-reserved-keys rule to support <script setup> (#1535)
* Update `vue/no-reserved-keys` rule to support `<script setup>` * support type-only props * update
1 parent ffb85b3 commit 11b2077

File tree

8 files changed

+417
-28
lines changed

8 files changed

+417
-28
lines changed

Diff for: lib/rules/no-reserved-keys.js

+43-22
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ module.exports = {
4040
},
4141
additionalProperties: false
4242
}
43-
]
43+
],
44+
messages: {
45+
reserved: "Key '{{name}}' is reserved.",
46+
startsWithUnderscore:
47+
"Keys starting with with '_' are reserved in '{{name}}' group."
48+
}
4449
},
4550
/** @param {RuleContext} context */
4651
create(context) {
@@ -52,28 +57,44 @@ module.exports = {
5257
// Public
5358
// ----------------------------------------------------------------------
5459

55-
return utils.executeOnVue(context, (obj) => {
56-
const properties = utils.iterateProperties(obj, groups)
57-
for (const o of properties) {
58-
if (o.groupName === 'data' && o.name[0] === '_') {
59-
context.report({
60-
node: o.node,
61-
message:
62-
"Keys starting with with '_' are reserved in '{{name}}' group.",
63-
data: {
64-
name: o.name
60+
return utils.compositingVisitors(
61+
utils.defineScriptSetupVisitor(context, {
62+
onDefinePropsEnter(_node, props) {
63+
for (const { propName, node } of props) {
64+
if (propName && reservedKeys.has(propName)) {
65+
context.report({
66+
node,
67+
messageId: 'reserved',
68+
data: {
69+
name: propName
70+
}
71+
})
6572
}
66-
})
67-
} else if (reservedKeys.has(o.name)) {
68-
context.report({
69-
node: o.node,
70-
message: "Key '{{name}}' is reserved.",
71-
data: {
72-
name: o.name
73-
}
74-
})
73+
}
7574
}
76-
}
77-
})
75+
}),
76+
utils.executeOnVue(context, (obj) => {
77+
const properties = utils.iterateProperties(obj, groups)
78+
for (const o of properties) {
79+
if (o.groupName === 'data' && o.name[0] === '_') {
80+
context.report({
81+
node: o.node,
82+
messageId: 'startsWithUnderscore',
83+
data: {
84+
name: o.name
85+
}
86+
})
87+
} else if (reservedKeys.has(o.name)) {
88+
context.report({
89+
node: o.node,
90+
messageId: 'reserved',
91+
data: {
92+
name: o.name
93+
}
94+
})
95+
}
96+
}
97+
})
98+
)
7899
}
79100
}

Diff for: lib/utils/index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
/**
1515
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayProp} ComponentArrayProp
1616
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectProp} ComponentObjectProp
17+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp
1718
*/
1819
/**
1920
* @typedef {object} ComponentArrayEmitDetectName
@@ -81,6 +82,7 @@ const path = require('path')
8182
const vueEslintParser = require('vue-eslint-parser')
8283
const traverseNodes = vueEslintParser.AST.traverseNodes
8384
const { findVariable } = require('eslint-utils')
85+
const { getComponentPropsFromTypeDefine } = require('./ts-ast-utils')
8486

8587
/**
8688
* @type { WeakMap<RuleContext, Token[]> }
@@ -1105,13 +1107,21 @@ module.exports = {
11051107
node.callee.type === 'Identifier' &&
11061108
node.callee.name === 'defineProps'
11071109
) {
1108-
/** @type {(ComponentArrayProp | ComponentObjectProp)[]} */
1110+
/** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */
11091111
let props = []
11101112
if (node.arguments.length >= 1) {
11111113
const defNode = getObjectOrArray(node.arguments[0])
11121114
if (defNode) {
11131115
props = getComponentPropsFromDefine(defNode)
11141116
}
1117+
} else if (
1118+
node.typeParameters &&
1119+
node.typeParameters.params.length >= 1
1120+
) {
1121+
props = getComponentPropsFromTypeDefine(
1122+
context,
1123+
node.typeParameters.params[0]
1124+
)
11151125
}
11161126
definePropsMap.set(node, props)
11171127
callVisitor('onDefinePropsEnter', node, props)

Diff for: lib/utils/ts-ast-utils.js

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
const { findVariable } = require('eslint-utils')
2+
/**
3+
* @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
4+
* @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSInterfaceBody
5+
* @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSTypeLiteral
6+
*/
7+
/**
8+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp
9+
*/
10+
11+
module.exports = {
12+
getComponentPropsFromTypeDefine
13+
}
14+
15+
/**
16+
* @param {TypeNode} node
17+
* @returns {node is TSTypeLiteral}
18+
*/
19+
function isTSTypeLiteral(node) {
20+
return node.type === 'TSTypeLiteral'
21+
}
22+
23+
/**
24+
* Get all props by looking at all component's properties
25+
* @param {RuleContext} context The ESLint rule context object.
26+
* @param {TypeNode} propsNode Type with props definition
27+
* @return {ComponentTypeProp[]} Array of component props
28+
*/
29+
function getComponentPropsFromTypeDefine(context, propsNode) {
30+
/** @type {TSInterfaceBody | TSTypeLiteral|null} */
31+
const defNode = resolveQualifiedType(context, propsNode, isTSTypeLiteral)
32+
if (!defNode) {
33+
return []
34+
}
35+
return [...extractRuntimeProps(context, defNode)]
36+
}
37+
38+
/**
39+
* @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512
40+
* @param {RuleContext} context The ESLint rule context object.
41+
* @param {TSTypeLiteral | TSInterfaceBody} node
42+
* @returns {IterableIterator<ComponentTypeProp>}
43+
*/
44+
function* extractRuntimeProps(context, node) {
45+
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
46+
for (const m of members) {
47+
if (
48+
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
49+
m.key.type === 'Identifier'
50+
) {
51+
let type
52+
if (m.type === 'TSMethodSignature') {
53+
type = ['Function']
54+
} else if (m.typeAnnotation) {
55+
type = inferRuntimeType(context, m.typeAnnotation.typeAnnotation)
56+
}
57+
yield {
58+
type: 'type',
59+
key: /** @type {Identifier} */ (m.key),
60+
propName: m.key.name,
61+
value: null,
62+
node: /** @type {TSPropertySignature | TSMethodSignature} */ (m),
63+
64+
required: !m.optional,
65+
types: type || [`null`]
66+
}
67+
}
68+
}
69+
}
70+
71+
/**
72+
* @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L425
73+
*
74+
* @param {RuleContext} context The ESLint rule context object.
75+
* @param {TypeNode} node
76+
* @param {(n: TypeNode)=> boolean } qualifier
77+
*/
78+
function resolveQualifiedType(context, node, qualifier) {
79+
if (qualifier(node)) {
80+
return node
81+
}
82+
if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') {
83+
const refName = node.typeName.name
84+
const variable = findVariable(context.getScope(), refName)
85+
if (variable && variable.defs.length === 1) {
86+
const def = variable.defs[0]
87+
if (def.node.type === 'TSInterfaceDeclaration') {
88+
return /** @type {any} */ (def.node).body
89+
}
90+
if (def.node.type === 'TSTypeAliasDeclaration') {
91+
const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation
92+
return qualifier(typeAnnotation) ? typeAnnotation : null
93+
}
94+
}
95+
}
96+
}
97+
98+
/**
99+
* @param {RuleContext} context The ESLint rule context object.
100+
* @param {TypeNode} node
101+
* @param {Set<TypeNode>} [checked]
102+
* @returns {string[]}
103+
*/
104+
function inferRuntimeType(context, node, checked = new Set()) {
105+
switch (node.type) {
106+
case 'TSStringKeyword':
107+
return ['String']
108+
case 'TSNumberKeyword':
109+
return ['Number']
110+
case 'TSBooleanKeyword':
111+
return ['Boolean']
112+
case 'TSObjectKeyword':
113+
return ['Object']
114+
case 'TSTypeLiteral':
115+
return ['Object']
116+
case 'TSFunctionType':
117+
return ['Function']
118+
case 'TSArrayType':
119+
case 'TSTupleType':
120+
return ['Array']
121+
122+
case 'TSLiteralType':
123+
switch (node.literal.type) {
124+
//@ts-ignore ?
125+
case 'StringLiteral':
126+
return ['String']
127+
//@ts-ignore ?
128+
case 'BooleanLiteral':
129+
return ['Boolean']
130+
//@ts-ignore ?
131+
case 'NumericLiteral':
132+
//@ts-ignore ?
133+
// eslint-disable-next-line no-fallthrough
134+
case 'BigIntLiteral':
135+
return ['Number']
136+
default:
137+
return [`null`]
138+
}
139+
140+
case 'TSTypeReference':
141+
if (node.typeName.type === 'Identifier') {
142+
const variable = findVariable(context.getScope(), node.typeName.name)
143+
if (variable && variable.defs.length === 1) {
144+
const def = variable.defs[0]
145+
if (def.node.type === 'TSInterfaceDeclaration') {
146+
return [`Object`]
147+
}
148+
if (def.node.type === 'TSTypeAliasDeclaration') {
149+
const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation
150+
if (!checked.has(typeAnnotation)) {
151+
checked.add(typeAnnotation)
152+
return inferRuntimeType(context, typeAnnotation, checked)
153+
}
154+
}
155+
}
156+
switch (node.typeName.name) {
157+
case 'Array':
158+
case 'Function':
159+
case 'Object':
160+
case 'Set':
161+
case 'Map':
162+
case 'WeakSet':
163+
case 'WeakMap':
164+
return [node.typeName.name]
165+
case 'Record':
166+
case 'Partial':
167+
case 'Readonly':
168+
case 'Pick':
169+
case 'Omit':
170+
case 'Exclude':
171+
case 'Extract':
172+
case 'Required':
173+
case 'InstanceType':
174+
return ['Object']
175+
}
176+
}
177+
return [`null`]
178+
179+
case 'TSUnionType':
180+
const set = new Set()
181+
for (const t of node.types) {
182+
for (const tt of inferRuntimeType(context, t, checked)) {
183+
set.add(tt)
184+
}
185+
}
186+
return [...set]
187+
188+
case 'TSIntersectionType':
189+
return ['Object']
190+
191+
default:
192+
return [`null`] // no runtime check
193+
}
194+
}

0 commit comments

Comments
 (0)