Skip to content

Commit

Permalink
feat: autofix in define-props-declaration: runtime syntax to type-b…
Browse files Browse the repository at this point in the history
…ased syntax (vuejs#2465)

additional tests and refactoring
  • Loading branch information
mpiniarski committed May 27, 2024
1 parent 583c0db commit 4499597
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 113 deletions.
239 changes: 127 additions & 112 deletions lib/rules/define-props-declaration.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,127 @@ const mapNativeType = (/** @type {string} */ nativeType) => {
}
}

/**
* @param {ComponentProp} prop
* @param {SourceCode} sourceCode
*/
function getComponentPropData(prop, sourceCode) {
const unknownType = {
name: prop.propName,
type: 'unknown',
required: undefined,
defaultValue: undefined
}

if (prop.type !== 'object') {
return unknownType
}
const type = optionGetType(prop.value, sourceCode)
if (type === null) {
return unknownType
}
const required = optionGetRequired(prop.value)
const defaultValue = optionGetDefault(prop.value)

return {
name: prop.propName,
type: mapNativeType(type),
required,
defaultValue
}
}

/**
* @param {Expression} node
* @param {SourceCode} sourceCode
* @returns {string | null}
*/
function optionGetType(node, sourceCode) {
switch (node.type) {
case 'Identifier': {
return node.name
}
case 'ObjectExpression': {
// foo: {
const typeProperty = utils.findProperty(node, 'type')
if (typeProperty == null) {
return null
}
if (typeProperty.value.type === 'TSAsExpression') {
const typeAnnotation = typeProperty.value.typeAnnotation
if (typeAnnotation.typeName.name !== 'PropType') {
return null
}

// in some project configuration parser populates deprecated field `typeParameters` instead of `typeArguments`
const typeArguments =
'typeArguments' in typeProperty.value
? typeAnnotation.typeArguments
: typeAnnotation.typeParameters

const typeArgument = Array.isArray(typeArguments)
? typeArguments[0].params[0]
: typeArguments.params[0]

if (typeArgument === undefined) {
return null
}

return sourceCode.getText(typeArgument)
}
return optionGetType(typeProperty.value, sourceCode)
}
case 'ArrayExpression': {
return null
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
return null
}
}

// Unknown
return null
}

/**
* @param {Expression} node
* @returns {boolean | undefined }
*/
function optionGetRequired(node) {
if (node.type === 'ObjectExpression') {
const requiredProperty = utils.findProperty(node, 'required')
if (requiredProperty == null) {
return undefined
}

if (requiredProperty.value.type === 'Literal') {
return Boolean(requiredProperty.value.value)
}
}

// Unknown
return undefined
}

/**
* @param {Expression} node
* @returns {Expression | undefined }
*/
function optionGetDefault(node) {
if (node.type === 'ObjectExpression') {
const defaultProperty = utils.findProperty(node, 'default')
if (defaultProperty == null) {
return undefined
}

return defaultProperty.value
}

// Unknown
return undefined
}

/**
* @typedef {import('../utils').ComponentProp} ComponentProp
*/
Expand Down Expand Up @@ -72,93 +193,6 @@ module.exports = {
create(context) {
const sourceCode = context.getSourceCode()

/**
* @param {Expression} node
* @returns {string | null}
*/
function optionGetType(node) {
switch (node.type) {
case 'Identifier': {
return node.name
}
case 'ObjectExpression': {
// foo: {
const typeProperty = utils.findProperty(node, 'type')
if (typeProperty == null) {
return null
}
if (typeProperty.value.type === 'TSAsExpression') {
if (
typeProperty.value.typeAnnotation.typeName.name !== 'PropType'
) {
return null
}

const typeArgument =
typeProperty.value.typeAnnotation.typeArguments.params[0]
if (typeArgument === undefined) {
return null
}

return sourceCode.getText(typeArgument)
}
return optionGetType(typeProperty.value)
}
case 'ArrayExpression': {
// foo: [
return null
// return node.elements.map((arrayElement) =>
// optionGetType(arrayElement)
// )
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
return null
}
}

// Unknown
return null
}

/**
* @param {Expression} node
* @returns {boolean | undefined }
*/
function optionGetRequired(node) {
if (node.type === 'ObjectExpression') {
const requiredProperty = utils.findProperty(node, 'required')
if (requiredProperty == null) {
return undefined
}

if (requiredProperty.value.type === 'Literal') {
return Boolean(requiredProperty.value.value)
}
}

// Unknown
return undefined
}

/**
* @param {Expression} node
* @returns {Expression | undefined }
*/
function optionGetDefault(node) {
if (node.type === 'ObjectExpression') {
const defaultProperty = utils.findProperty(node, 'default')
if (defaultProperty == null) {
return undefined
}

return defaultProperty.value
}

// Unknown
return undefined
}

const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup || !utils.hasAttribute(scriptSetup, 'lang', 'ts')) {
return {}
Expand All @@ -176,31 +210,9 @@ module.exports = {
node,
messageId: 'hasArg',
*fix(fixer) {
const propTypes = props.map((prop) => {
const unknownType = {
name: prop.propName,
type: 'unknown',
required: undefined,
defaultValue: undefined
}

if (prop.type !== 'object') {
return unknownType
}
const type = optionGetType(prop.value)
if (type === null) {
return unknownType
}
const required = optionGetRequired(prop.value)
const defaultValue = optionGetDefault(prop.value)

return {
name: prop.propName,
type: mapNativeType(type),
required,
defaultValue
}
})
const propTypes = props.map((prop) =>
getComponentPropData(prop, sourceCode)
)

const definePropsType = `{ ${propTypes
.map(
Expand All @@ -209,8 +221,10 @@ module.exports = {
)
.join(', ')} }`

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

// add type annotation
if (separateInterface) {
const variableDeclarationNode = node.parent.parent
if (!variableDeclarationNode) return
Expand All @@ -227,6 +241,7 @@ module.exports = {
)
}

// add defaults if needed
const defaults = propTypes.filter(
({ defaultValue }) => defaultValue
)
Expand Down
80 changes: 79 additions & 1 deletion tests/lib/rules/define-props-declaration.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/define-props-declaration')
const tester = new RuleTester({
languageOptions: {
parser: require('vue-eslint-parser'),
ecmaVersion: 'latest',
ecmaVersion: '2020',
sourceType: 'module',
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
Expand Down Expand Up @@ -289,6 +289,28 @@ tester.run('define-props-declaration', rule, {
}
]
},
// Custom type
{
filename: 'test.vue',
code: `
<script setup lang="ts">
const props = defineProps({
kind: User
})
</script>
`,
output: `
<script setup lang="ts">
const props = defineProps<{ kind: User }>()
</script>
`,
errors: [
{
message: 'Use type-based declaration instead of runtime declaration.',
line: 3
}
]
},
// Native Type with PropType
{
filename: 'test.vue',
Expand Down Expand Up @@ -337,6 +359,62 @@ tester.run('define-props-declaration', rule, {
}
]
},
// Object with PropType with separate type
{
filename: 'test.vue',
code: `
<script setup lang="ts">
interface Kind { id: number; name: string }
const props = defineProps({
kind: {
type: Object as PropType<Kind>,
}
})
</script>
`,
output: `
<script setup lang="ts">
interface Kind { id: number; name: string }
const props = defineProps<{ kind: Kind }>()
</script>
`,
errors: [
{
message: 'Use type-based declaration instead of runtime declaration.',
line: 5
}
]
},
// Object with PropType with separate imported type
{
filename: 'test.vue',
code: `
<script setup lang="ts">
import Kind from 'test'
const props = defineProps({
kind: {
type: Object as PropType<Kind>,
}
})
</script>
`,
output: `
<script setup lang="ts">
import Kind from 'test'
const props = defineProps<{ kind: Kind }>()
</script>
`,
errors: [
{
message: 'Use type-based declaration instead of runtime declaration.',
line: 5
}
]
},
// Array with PropType
{
filename: 'test.vue',
Expand Down

0 comments on commit 4499597

Please sign in to comment.