You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
126 lines
3.6 KiB
126 lines
3.6 KiB
1 month ago
|
/**
|
||
|
* @fileoverview detect if there is a potential typo in your component property
|
||
|
* @author IWANABETHATGUY
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
const utils = require('../utils')
|
||
|
const vueComponentOptions = require('../utils/vue-component-options.json')
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'disallow a potential typo in your component property',
|
||
|
categories: undefined,
|
||
|
recommended: false,
|
||
|
url: 'https://eslint.vuejs.org/rules/no-potential-component-option-typo.html'
|
||
|
},
|
||
|
fixable: null,
|
||
|
hasSuggestions: true,
|
||
|
schema: [
|
||
|
{
|
||
|
type: 'object',
|
||
|
properties: {
|
||
|
presets: {
|
||
|
type: 'array',
|
||
|
items: {
|
||
|
type: 'string',
|
||
|
enum: ['all', 'vue', 'vue-router', 'nuxt']
|
||
|
},
|
||
|
uniqueItems: true,
|
||
|
minItems: 0
|
||
|
},
|
||
|
custom: {
|
||
|
type: 'array',
|
||
|
minItems: 0,
|
||
|
items: { type: 'string' },
|
||
|
uniqueItems: true
|
||
|
},
|
||
|
threshold: {
|
||
|
type: 'number',
|
||
|
minimum: 1
|
||
|
}
|
||
|
},
|
||
|
additionalProperties: false
|
||
|
}
|
||
|
],
|
||
|
messages: {
|
||
|
potentialTypo: `'{{name}}' may be a typo, which is similar to option [{{option}}].`
|
||
|
}
|
||
|
},
|
||
|
/** @param {RuleContext} context */
|
||
|
create(context) {
|
||
|
const option = context.options[0] || {}
|
||
|
const custom = option.custom || []
|
||
|
/** @type {('all' | 'vue' | 'vue-router' | 'nuxt')[]} */
|
||
|
const presets = option.presets || ['vue']
|
||
|
const threshold = option.threshold || 1
|
||
|
/** @type {Set<string>} */
|
||
|
const candidateOptionSet = new Set(custom)
|
||
|
for (const preset of presets) {
|
||
|
if (preset === 'all') {
|
||
|
for (const opts of Object.values(vueComponentOptions)) {
|
||
|
for (const opt of opts) {
|
||
|
candidateOptionSet.add(opt)
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
for (const opt of vueComponentOptions[preset]) {
|
||
|
candidateOptionSet.add(opt)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const candidateOptionList = [...candidateOptionSet]
|
||
|
if (candidateOptionList.length === 0) {
|
||
|
return {}
|
||
|
}
|
||
|
return utils.executeOnVue(context, (obj) => {
|
||
|
const componentInstanceOptions = obj.properties
|
||
|
.map((p) => {
|
||
|
if (p.type === 'Property') {
|
||
|
const name = utils.getStaticPropertyName(p)
|
||
|
if (name != null) {
|
||
|
return {
|
||
|
name,
|
||
|
key: p.key
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null
|
||
|
})
|
||
|
.filter(utils.isDef)
|
||
|
|
||
|
if (componentInstanceOptions.length === 0) {
|
||
|
return
|
||
|
}
|
||
|
for (const option of componentInstanceOptions) {
|
||
|
const id = option.key
|
||
|
const name = option.name
|
||
|
if (candidateOptionSet.has(name)) {
|
||
|
continue
|
||
|
}
|
||
|
const potentialTypoList = candidateOptionList
|
||
|
.map((o) => ({ option: o, distance: utils.editDistance(o, name) }))
|
||
|
.filter(({ distance }) => distance <= threshold && distance > 0)
|
||
|
.sort((a, b) => a.distance - b.distance)
|
||
|
if (potentialTypoList.length > 0) {
|
||
|
context.report({
|
||
|
node: id,
|
||
|
messageId: 'potentialTypo',
|
||
|
data: {
|
||
|
name,
|
||
|
option: potentialTypoList.map(({ option }) => option).join(',')
|
||
|
},
|
||
|
suggest: potentialTypoList.map(({ option }) => ({
|
||
|
desc: `Replace property '${name}' to '${option}'`,
|
||
|
fix(fixer) {
|
||
|
return fixer.replaceText(id, option)
|
||
|
}
|
||
|
}))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|