Skip to content

Commit

Permalink
Update vue/no-reserved-keys rule to support <script setup> (#1535)
Browse files Browse the repository at this point in the history
* Update `vue/no-reserved-keys` rule to support `<script setup>`

* support type-only props

* update
  • Loading branch information
ota-meshi committed Jul 2, 2021
1 parent ffb85b3 commit 11b2077
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 28 deletions.
65 changes: 43 additions & 22 deletions lib/rules/no-reserved-keys.js
Expand Up @@ -40,7 +40,12 @@ module.exports = {
},
additionalProperties: false
}
]
],
messages: {
reserved: "Key '{{name}}' is reserved.",
startsWithUnderscore:
"Keys starting with with '_' are reserved in '{{name}}' group."
}
},
/** @param {RuleContext} context */
create(context) {
Expand All @@ -52,28 +57,44 @@ module.exports = {
// Public
// ----------------------------------------------------------------------

return utils.executeOnVue(context, (obj) => {
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (o.groupName === 'data' && o.name[0] === '_') {
context.report({
node: o.node,
message:
"Keys starting with with '_' are reserved in '{{name}}' group.",
data: {
name: o.name
return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(_node, props) {
for (const { propName, node } of props) {
if (propName && reservedKeys.has(propName)) {
context.report({
node,
messageId: 'reserved',
data: {
name: propName
}
})
}
})
} else if (reservedKeys.has(o.name)) {
context.report({
node: o.node,
message: "Key '{{name}}' is reserved.",
data: {
name: o.name
}
})
}
}
}
})
}),
utils.executeOnVue(context, (obj) => {
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (o.groupName === 'data' && o.name[0] === '_') {
context.report({
node: o.node,
messageId: 'startsWithUnderscore',
data: {
name: o.name
}
})
} else if (reservedKeys.has(o.name)) {
context.report({
node: o.node,
messageId: 'reserved',
data: {
name: o.name
}
})
}
}
})
)
}
}
12 changes: 11 additions & 1 deletion lib/utils/index.js
Expand Up @@ -14,6 +14,7 @@
/**
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayProp} ComponentArrayProp
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectProp} ComponentObjectProp
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp
*/
/**
* @typedef {object} ComponentArrayEmitDetectName
Expand Down Expand Up @@ -81,6 +82,7 @@ const path = require('path')
const vueEslintParser = require('vue-eslint-parser')
const traverseNodes = vueEslintParser.AST.traverseNodes
const { findVariable } = require('eslint-utils')
const { getComponentPropsFromTypeDefine } = require('./ts-ast-utils')

/**
* @type { WeakMap<RuleContext, Token[]> }
Expand Down Expand Up @@ -1105,13 +1107,21 @@ module.exports = {
node.callee.type === 'Identifier' &&
node.callee.name === 'defineProps'
) {
/** @type {(ComponentArrayProp | ComponentObjectProp)[]} */
/** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */
let props = []
if (node.arguments.length >= 1) {
const defNode = getObjectOrArray(node.arguments[0])
if (defNode) {
props = getComponentPropsFromDefine(defNode)
}
} else if (
node.typeParameters &&
node.typeParameters.params.length >= 1
) {
props = getComponentPropsFromTypeDefine(
context,
node.typeParameters.params[0]
)
}
definePropsMap.set(node, props)
callVisitor('onDefinePropsEnter', node, props)
Expand Down
194 changes: 194 additions & 0 deletions lib/utils/ts-ast-utils.js
@@ -0,0 +1,194 @@
const { findVariable } = require('eslint-utils')
/**
* @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
* @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSInterfaceBody
* @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSTypeLiteral
*/
/**
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp
*/

module.exports = {
getComponentPropsFromTypeDefine
}

/**
* @param {TypeNode} node
* @returns {node is TSTypeLiteral}
*/
function isTSTypeLiteral(node) {
return node.type === 'TSTypeLiteral'
}

/**
* Get all props by looking at all component's properties
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode} propsNode Type with props definition
* @return {ComponentTypeProp[]} Array of component props
*/
function getComponentPropsFromTypeDefine(context, propsNode) {
/** @type {TSInterfaceBody | TSTypeLiteral|null} */
const defNode = resolveQualifiedType(context, propsNode, isTSTypeLiteral)
if (!defNode) {
return []
}
return [...extractRuntimeProps(context, defNode)]
}

/**
* @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512
* @param {RuleContext} context The ESLint rule context object.
* @param {TSTypeLiteral | TSInterfaceBody} node
* @returns {IterableIterator<ComponentTypeProp>}
*/
function* extractRuntimeProps(context, node) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
if (
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
m.key.type === 'Identifier'
) {
let type
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
type = inferRuntimeType(context, m.typeAnnotation.typeAnnotation)
}
yield {
type: 'type',
key: /** @type {Identifier} */ (m.key),
propName: m.key.name,
value: null,
node: /** @type {TSPropertySignature | TSMethodSignature} */ (m),

required: !m.optional,
types: type || [`null`]
}
}
}
}

/**
* @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L425
*
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode} node
* @param {(n: TypeNode)=> boolean } qualifier
*/
function resolveQualifiedType(context, node, qualifier) {
if (qualifier(node)) {
return node
}
if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') {
const refName = node.typeName.name
const variable = findVariable(context.getScope(), refName)
if (variable && variable.defs.length === 1) {
const def = variable.defs[0]
if (def.node.type === 'TSInterfaceDeclaration') {
return /** @type {any} */ (def.node).body
}
if (def.node.type === 'TSTypeAliasDeclaration') {
const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation
return qualifier(typeAnnotation) ? typeAnnotation : null
}
}
}
}

/**
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode} node
* @param {Set<TypeNode>} [checked]
* @returns {string[]}
*/
function inferRuntimeType(context, node, checked = new Set()) {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSTypeLiteral':
return ['Object']
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
return ['Array']

case 'TSLiteralType':
switch (node.literal.type) {
//@ts-ignore ?
case 'StringLiteral':
return ['String']
//@ts-ignore ?
case 'BooleanLiteral':
return ['Boolean']
//@ts-ignore ?
case 'NumericLiteral':
//@ts-ignore ?
// eslint-disable-next-line no-fallthrough
case 'BigIntLiteral':
return ['Number']
default:
return [`null`]
}

case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
const variable = findVariable(context.getScope(), node.typeName.name)
if (variable && variable.defs.length === 1) {
const def = variable.defs[0]
if (def.node.type === 'TSInterfaceDeclaration') {
return [`Object`]
}
if (def.node.type === 'TSTypeAliasDeclaration') {
const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation
if (!checked.has(typeAnnotation)) {
checked.add(typeAnnotation)
return inferRuntimeType(context, typeAnnotation, checked)
}
}
}
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
return [node.typeName.name]
case 'Record':
case 'Partial':
case 'Readonly':
case 'Pick':
case 'Omit':
case 'Exclude':
case 'Extract':
case 'Required':
case 'InstanceType':
return ['Object']
}
}
return [`null`]

case 'TSUnionType':
const set = new Set()
for (const t of node.types) {
for (const tt of inferRuntimeType(context, t, checked)) {
set.add(tt)
}
}
return [...set]

case 'TSIntersectionType':
return ['Object']

default:
return [`null`] // no runtime check
}
}

0 comments on commit 11b2077

Please sign in to comment.