Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update vue/no-reserved-keys rule to support <script setup> #1535

Merged
merged 3 commits into from Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}
}