first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

View File

@ -0,0 +1,27 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=2.6.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports dynamic argument node
* @param {VExpressionContainer} dynamicArgument node of dynamic argument
* @returns {void}
*/
function reportDynamicArgument(dynamicArgument) {
context.report({
node: dynamicArgument,
messageId: 'forbiddenDynamicDirectiveArguments'
})
}
return {
'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer':
reportDynamicArgument
}
}
}

View File

@ -0,0 +1,25 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=3.1.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
return {
/** @param {VAttribute} node */
"VAttribute[directive=false][key.name='is']"(node) {
if (!node.value) {
return
}
if (node.value.value.startsWith('vue:')) {
context.report({
node: node.value,
messageId: 'forbiddenIsAttributeWithVuePrefix'
})
}
}
}
}
}

View File

@ -0,0 +1,30 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '2.5.0',
supported: '<3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `scope` node
* @param {VDirectiveKey} scopeKey node of `scope`
* @returns {void}
*/
function reportScope(scopeKey) {
context.report({
node: scopeKey,
messageId: 'forbiddenScopeAttribute',
// fix to use `slot-scope`
fix: (fixer) => fixer.replaceText(scopeKey, 'slot-scope')
})
}
return {
"VAttribute[directive=true] > VDirectiveKey[name.name='scope']":
reportScope
}
}
}

View File

@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../../utils')
module.exports = {
supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createScriptVisitor(context) {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}
const reportNode =
utils.getAttribute(scriptSetup, 'setup') || scriptSetup.startTag
return {
Program() {
context.report({
node: reportNode,
messageId: 'forbiddenScriptSetup'
})
}
}
}
}

View File

@ -0,0 +1,141 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const canConvertToVSlot = require('./utils/can-convert-to-v-slot')
module.exports = {
deprecated: '2.6.0',
supported: '<3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
const sourceCode = context.getSourceCode()
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VAttribute} slotAttr node of `slot`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromSlotToVSlot(slotAttr) {
if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
return false
}
if (!slotAttr.value) {
return true
}
const slotName = slotAttr.value.value
// If other than alphanumeric, underscore and hyphen characters are included it can not be converted.
return !/[^\w\-]/u.test(slotName)
}
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VDirective} slotAttr node of `v-bind:slot`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromVBindSlotToVSlot(slotAttr) {
if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
return false
}
if (!slotAttr.value) {
return true
}
if (!slotAttr.value.expression) {
// parse error or empty expression
return false
}
const slotName = sourceCode.getText(slotAttr.value.expression).trim()
// If non-Latin characters are included it can not be converted.
// It does not check the space only because `a>b?c:d` should be rejected.
return !/[^a-z]/i.test(slotName)
}
/**
* Convert to `v-slot`.
* @param {RuleFixer} fixer fixer
* @param {VAttribute|VDirective} slotAttr node of `slot`
* @param {string | null} slotName name of `slot`
* @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
* @returns {IterableIterator<Fix>} fix data
*/
function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
const element = slotAttr.parent
const scopeAttr = element.attributes.find(
(attr) =>
attr.directive === true &&
attr.key.name &&
(attr.key.name.name === 'slot-scope' ||
attr.key.name.name === 'scope')
)
const nameArgument = slotName
? vBind
? `:[${slotName}]`
: `:${slotName}`
: ''
const scopeValue =
scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${nameArgument}${scopeValue}`
yield fixer.replaceText(slotAttr || scopeAttr, replaceText)
if (slotAttr && scopeAttr) {
yield fixer.remove(scopeAttr)
}
}
/**
* Reports `slot` node
* @param {VAttribute} slotAttr node of `slot`
* @returns {void}
*/
function reportSlot(slotAttr) {
context.report({
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
*fix(fixer) {
if (!canConvertFromSlotToVSlot(slotAttr)) {
return
}
const slotName = slotAttr.value && slotAttr.value.value
yield* fixSlotToVSlot(fixer, slotAttr, slotName, false)
}
})
}
/**
* Reports `v-bind:slot` node
* @param {VDirective} slotAttr node of `v-bind:slot`
* @returns {void}
*/
function reportVBindSlot(slotAttr) {
context.report({
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
*fix(fixer) {
if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
return
}
const slotName =
slotAttr.value &&
slotAttr.value.expression &&
sourceCode.getText(slotAttr.value.expression).trim()
yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
}
})
}
return {
"VAttribute[directive=false][key.name='slot']": reportSlot,
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']":
reportVBindSlot
}
}
}

View File

@ -0,0 +1,103 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const canConvertToVSlotForElement = require('./utils/can-convert-to-v-slot')
module.exports = {
deprecated: '2.6.0',
supported: '>=2.5.0 <3.0.0',
/**
* @param {RuleContext} context
* @param {object} option
* @param {boolean} [option.fixToUpgrade]
* @returns {TemplateListener}
*/
createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
const sourceCode = context.getSourceCode()
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VStartTag} startTag node of `<element v-slot ... >`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertToVSlot(startTag) {
if (
!canConvertToVSlotForElement(startTag.parent, sourceCode, tokenStore)
) {
return false
}
const slotAttr = startTag.attributes.find(
(attr) => attr.directive === false && attr.key.name === 'slot'
)
if (slotAttr) {
// if the element have `slot` it can not be converted.
// Conversion of `slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}
const vBindSlotAttr = startTag.attributes.find(
(attr) =>
attr.directive === true &&
attr.key.name.name === 'bind' &&
attr.key.argument &&
attr.key.argument.type === 'VIdentifier' &&
attr.key.argument.name === 'slot'
)
if (vBindSlotAttr) {
// if the element have `v-bind:slot` it can not be converted.
// Conversion of `v-bind:slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}
return true
}
/**
* Convert to `v-slot`.
* @param {RuleFixer} fixer fixer
* @param {VDirective} scopeAttr node of `slot-scope`
* @returns {Fix} fix data
*/
function fixSlotScopeToVSlot(fixer, scopeAttr) {
const scopeValue =
scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${scopeValue}`
return fixer.replaceText(scopeAttr, replaceText)
}
/**
* Reports `slot-scope` node
* @param {VDirective} scopeAttr node of `slot-scope`
* @returns {void}
*/
function reportSlotScope(scopeAttr) {
context.report({
node: scopeAttr.key,
messageId: 'forbiddenSlotScopeAttribute',
fix(fixer) {
if (!fixToUpgrade) {
return null
}
// fix to use `v-slot`
const startTag = scopeAttr.parent
if (!canConvertToVSlot(startTag)) {
return null
}
return fixSlotScopeToVSlot(fixer, scopeAttr)
}
})
}
return {
"VAttribute[directive=true][key.name.name='slot-scope']": reportSlotScope
}
}
}

View File

@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const { getStyleVariablesContext } = require('../../utils/style-variables')
module.exports = {
supported: '>=3.0.3',
/** @param {RuleContext} context @returns {TemplateListener} */
createScriptVisitor(context) {
const styleVars = getStyleVariablesContext(context)
if (!styleVars) {
return {}
}
return {
Program() {
for (const vBind of styleVars.vBinds) {
context.report({
node: vBind,
messageId: 'forbiddenStyleCssVarsInjection'
})
}
}
}
}
}

View File

@ -0,0 +1,228 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../../../utils')
/**
* @typedef {object} SlotVForVariables
* @property {VForExpression} expr
* @property {VVariable[]} variables
*/
/**
* @typedef {object} SlotContext
* @property {VElement} element
* @property {VAttribute | VDirective | null} slot
* @property {VDirective | null} vFor
* @property {SlotVForVariables | null} slotVForVars
* @property {string} normalizedName
*/
/**
* Checks whether the given element can use v-slot.
* @param {VElement} element
* @param {SourceCode} sourceCode
* @param {ParserServices.TokenStore} tokenStore
*/
module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) {
if (element.name !== 'template') {
return false
}
const ownerElement = element.parent
if (
ownerElement.type === 'VDocumentFragment' ||
!utils.isCustomComponent(ownerElement)
) {
return false
}
const slot = getSlotContext(element, sourceCode)
if (slot.vFor && !slot.slotVForVars) {
// E.g., <template v-for="x of xs" #one></template>
return false
}
if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) {
return false
}
return true
}
/**
* @param {VElement} element
* @param {SourceCode} sourceCode
* @returns {SlotContext}
*/
function getSlotContext(element, sourceCode) {
const slot =
utils.getAttribute(element, 'slot') ||
utils.getDirective(element, 'bind', 'slot')
const vFor = utils.getDirective(element, 'for')
const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor)
return {
element,
slot,
vFor,
slotVForVars,
normalizedName: getNormalizedName(slot, sourceCode)
}
}
/**
* Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute.
* @param {VAttribute | VDirective | null} slot The current `slot` attribute node.
* @param {VDirective | null} [vFor] The current `v-for` directive node.
* @returns { SlotVForVariables | null } The SlotVForVariables.
*/
function getSlotVForVariableIfUsingIterationVars(slot, vFor) {
if (!slot || !slot.directive) {
return null
}
const expr =
vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
const variables =
expr && getUsingIterationVars(slot.value, slot.parent.parent)
return expr && variables && variables.length ? { expr, variables } : null
}
/**
* Gets iterative variables if a given expression node is using iterative variables that the element defined.
* @param {VExpressionContainer|null} expression The expression node to check.
* @param {VElement} element The element node which has the expression.
* @returns {VVariable[]} The expression node is using iteration variables.
*/
function getUsingIterationVars(expression, element) {
const vars = []
if (expression && expression.type === 'VExpressionContainer') {
for (const { variable } of expression.references) {
if (
variable != null &&
variable.kind === 'v-for' &&
variable.id.range[0] > element.startTag.range[0] &&
variable.id.range[1] < element.startTag.range[1]
) {
vars.push(variable)
}
}
}
return vars
}
/**
* Get the normalized name of a given `slot` attribute node.
* @param {VAttribute | VDirective | null} slotAttr node of `slot`
* @param {SourceCode} sourceCode The source code.
* @returns {string} The normalized name.
*/
function getNormalizedName(slotAttr, sourceCode) {
if (!slotAttr) {
return 'default'
}
if (!slotAttr.directive) {
return slotAttr.value ? slotAttr.value.value : 'default'
}
return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]'
}
/**
* Checks whether parent element has the same slot as the given slot.
* @param {VElement} ownerElement The parent element.
* @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same.
* @param {SourceCode} sourceCode
* @param {ParserServices.TokenStore} tokenStore
*/
function hasSameSlotDirective(
ownerElement,
targetSlot,
sourceCode,
tokenStore
) {
for (const group of utils.iterateChildElementsChains(ownerElement)) {
if (group.includes(targetSlot.element)) {
continue
}
for (const childElement of group) {
const slot = getSlotContext(childElement, sourceCode)
if (!targetSlot.slotVForVars || !slot.slotVForVars) {
if (
!targetSlot.slotVForVars &&
!slot.slotVForVars &&
targetSlot.normalizedName === slot.normalizedName
) {
return true
}
continue
}
if (
equalSlotVForVariables(
targetSlot.slotVForVars,
slot.slotVForVars,
tokenStore
)
) {
return true
}
}
}
return false
}
/**
* Determines whether the two given `v-slot` variables are considered to be equal.
* @param {SlotVForVariables} a First element.
* @param {SlotVForVariables} b Second element.
* @param {ParserServices.TokenStore} tokenStore The token store.
* @returns {boolean} `true` if the elements are considered to be equal.
*/
function equalSlotVForVariables(a, b, tokenStore) {
if (a.variables.length !== b.variables.length) {
return false
}
if (!equal(a.expr.right, b.expr.right)) {
return false
}
const checkedVarNames = new Set()
const len = Math.min(a.expr.left.length, b.expr.left.length)
for (let index = 0; index < len; index++) {
const aPtn = a.expr.left[index]
const bPtn = b.expr.left[index]
const aVar = a.variables.find(
(v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
)
const bVar = b.variables.find(
(v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
)
if (aVar && bVar) {
if (aVar.id.name !== bVar.id.name) {
return false
}
if (!equal(aPtn, bPtn)) {
return false
}
checkedVarNames.add(aVar.id.name)
} else if (aVar || bVar) {
return false
}
}
for (const v of a.variables) {
if (!checkedVarNames.has(v.id.name)) {
if (b.variables.every((bv) => v.id.name !== bv.id.name)) {
return false
}
}
}
return true
/**
* Determines whether the two given nodes are considered to be equal.
* @param {ASTNode} a First node.
* @param {ASTNode} b Second node.
* @returns {boolean} `true` if the nodes are considered to be equal.
*/
function equal(a, b) {
if (a.type !== b.type) {
return false
}
return utils.equalTokens(a, b, tokenStore)
}
}

View File

@ -0,0 +1,32 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=3.2.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `v-bind.attr` node
* @param { VIdentifier } mod node of `v-bind.attr`
* @returns {void}
*/
function report(mod) {
context.report({
node: mod,
messageId: 'forbiddenVBindAttrModifier'
})
}
return {
"VAttribute[directive=true][key.name.name='bind']"(node) {
const attrMod = node.key.modifiers.find((m) => m.name === 'attr')
if (attrMod) {
report(attrMod)
}
}
}
}
}

View File

@ -0,0 +1,34 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=3.2.0 || >=2.6.0-beta.1 <=2.6.0-beta.3',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `.prop` shorthand node
* @param { VDirectiveKey & { argument: VIdentifier } } bindPropKey node of `.prop` shorthand
* @returns {void}
*/
function reportPropModifierShorthand(bindPropKey) {
context.report({
node: bindPropKey,
messageId: 'forbiddenVBindPropModifierShorthand',
// fix to use `:x.prop` (downgrade)
fix: (fixer) =>
fixer.replaceText(
bindPropKey,
`:${bindPropKey.argument.rawName}.prop`
)
})
}
return {
"VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']":
reportPropModifierShorthand
}
}
}

View File

@ -0,0 +1,27 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '3.1.0',
supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `v-is` node
* @param {VDirective} vIsAttr node of `v-is`
* @returns {void}
*/
function reportVIs(vIsAttr) {
context.report({
node: vIsAttr.key,
messageId: 'forbiddenVIs'
})
}
return {
"VAttribute[directive=true][key.name.name='is']": reportVIs
}
}
}

View File

@ -0,0 +1,26 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=3.2.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `v-is` node
* @param {VDirective} vMemoAttr node of `v-is`
* @returns {void}
*/
function reportVMemo(vMemoAttr) {
context.report({
node: vMemoAttr.key,
messageId: 'forbiddenVMemo'
})
}
return {
"VAttribute[directive=true][key.name.name='memo']": reportVMemo
}
}
}

View File

@ -0,0 +1,23 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
return {
/** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
"VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
node
) {
context.report({
node: node.argument,
messageId: 'forbiddenVModelArgument'
})
}
}
}
}

View File

@ -0,0 +1,33 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
module.exports = {
supported: '>=3.0.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
return {
/** @param {VDirectiveKey} node */
"VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
node
) {
for (const modifier of node.modifiers) {
if (!BUILTIN_MODIFIERS.has(modifier.name)) {
context.report({
node: modifier,
messageId: 'forbiddenVModelCustomModifiers'
})
}
}
}
}
}
}

View File

@ -0,0 +1,85 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '>=2.6.0',
/** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
const sourceCode = context.getSourceCode()
/**
* Checks whether the given node can convert to the `slot`.
* @param {VDirective} vSlotAttr node of `v-slot`
* @returns {boolean} `true` if the given node can convert to the `slot`
*/
function canConvertToSlot(vSlotAttr) {
if (vSlotAttr.parent.parent.name !== 'template') {
return false
}
return true
}
/**
* Convert to `slot` and `slot-scope`.
* @param {RuleFixer} fixer fixer
* @param {VDirective} vSlotAttr node of `v-slot`
* @returns {null|Fix} fix data
*/
function fixVSlotToSlot(fixer, vSlotAttr) {
const key = vSlotAttr.key
if (key.modifiers.length) {
// unknown modifiers
return null
}
const attrs = []
const argument = key.argument
if (argument) {
if (argument.type === 'VIdentifier') {
const name = argument.rawName
attrs.push(`slot="${name}"`)
} else if (
argument.type === 'VExpressionContainer' &&
argument.expression
) {
const expression = sourceCode.getText(argument.expression)
attrs.push(`:slot="${expression}"`)
} else {
// unknown or syntax error
return null
}
}
const scopedValueNode = vSlotAttr.value
if (scopedValueNode) {
attrs.push(`slot-scope=${sourceCode.getText(scopedValueNode)}`)
}
if (!attrs.length) {
attrs.push('slot') // useless
}
return fixer.replaceText(vSlotAttr, attrs.join(' '))
}
/**
* Reports `v-slot` node
* @param {VDirective} vSlotAttr node of `v-slot`
* @returns {void}
*/
function reportVSlot(vSlotAttr) {
context.report({
node: vSlotAttr.key,
messageId: 'forbiddenVSlot',
// fix to use `slot` (downgrade)
fix(fixer) {
if (!canConvertToSlot(vSlotAttr)) {
return null
}
return fixVSlotToSlot(fixer, vSlotAttr)
}
})
}
return {
"VAttribute[directive=true][key.name.name='slot']": reportVSlot
}
}
}