eslint-plugin-vue
Advanced tools
Comparing version 9.31.0 to 9.32.0
@@ -234,2 +234,3 @@ /* | ||
'require-valid-default-prop': require('./rules/require-valid-default-prop'), | ||
'restricted-component-names': require('./rules/restricted-component-names'), | ||
'return-in-computed-property': require('./rules/return-in-computed-property'), | ||
@@ -240,2 +241,3 @@ 'return-in-emits-validator': require('./rules/return-in-emits-validator'), | ||
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'), | ||
'slot-name-casing': require('./rules/slot-name-casing'), | ||
'sort-keys': require('./rules/sort-keys'), | ||
@@ -242,0 +244,0 @@ 'space-in-parens': require('./rules/space-in-parens'), |
@@ -9,2 +9,3 @@ /** | ||
const casing = require('../utils/casing') | ||
const { toRegExp } = require('../utils/regexp') | ||
const svgAttributes = require('../utils/svg-attributes-weird-case.json') | ||
@@ -60,2 +61,8 @@ | ||
additionalItems: false | ||
}, | ||
ignoreTags: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
uniqueItems: true, | ||
additionalItems: false | ||
} | ||
@@ -77,2 +84,7 @@ }, | ||
const useHyphenated = option !== 'never' | ||
/** @type {RegExp[]} */ | ||
const ignoredTagsRegexps = ( | ||
(optionsPayload && optionsPayload.ignoreTags) || | ||
[] | ||
).map(toRegExp) | ||
const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes] | ||
@@ -136,7 +148,13 @@ | ||
/** @param {string} name */ | ||
function isIgnoredTagName(name) { | ||
return ignoredTagsRegexps.some((re) => re.test(name)) | ||
} | ||
return utils.defineTemplateBodyVisitor(context, { | ||
VAttribute(node) { | ||
const element = node.parent.parent | ||
if ( | ||
!utils.isCustomComponent(node.parent.parent) && | ||
node.parent.parent.name !== 'slot' | ||
(!utils.isCustomComponent(element) && element.name !== 'slot') || | ||
isIgnoredTagName(element.rawName) | ||
) | ||
@@ -143,0 +161,0 @@ return |
@@ -9,2 +9,29 @@ /** | ||
/** @param {VElement[]} elements */ | ||
function isConditionalGroup(elements) { | ||
if (elements.length < 2) { | ||
return false | ||
} | ||
const firstElement = elements[0] | ||
const lastElement = elements[elements.length - 1] | ||
const inBetweenElements = elements.slice(1, -1) | ||
return ( | ||
utils.hasDirective(firstElement, 'if') && | ||
(utils.hasDirective(lastElement, 'else-if') || | ||
utils.hasDirective(lastElement, 'else')) && | ||
inBetweenElements.every((element) => utils.hasDirective(element, 'else-if')) | ||
) | ||
} | ||
/** @param {VElement[]} elements */ | ||
function isMultiRootNodes(elements) { | ||
if (elements.length > 1 && !isConditionalGroup(elements)) { | ||
return true | ||
} | ||
return false | ||
} | ||
module.exports = { | ||
@@ -21,3 +48,13 @@ meta: { | ||
fixable: null, | ||
schema: [], | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
checkMultiRootNodes: { | ||
type: 'boolean' | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
], | ||
messages: { | ||
@@ -29,4 +66,9 @@ noDuplicateAttrInheritance: 'Set "inheritAttrs" to false.' | ||
create(context) { | ||
const options = context.options[0] || {} | ||
const checkMultiRootNodes = options.checkMultiRootNodes === true | ||
/** @type {string | number | boolean | RegExp | BigInt | null} */ | ||
let inheritsAttrs = true | ||
/** @type {VReference[]} */ | ||
const attrsRefs = [] | ||
@@ -60,3 +102,3 @@ /** @param {ObjectExpression} node */ | ||
} | ||
const attrsRef = node.references.find((reference) => { | ||
const reference = node.references.find((reference) => { | ||
if (reference.variable != null) { | ||
@@ -69,12 +111,30 @@ // Not vm reference | ||
if (attrsRef) { | ||
context.report({ | ||
node: attrsRef.id, | ||
messageId: 'noDuplicateAttrInheritance' | ||
}) | ||
if (reference) { | ||
attrsRefs.push(reference) | ||
} | ||
} | ||
}) | ||
}), | ||
{ | ||
'Program:exit'(program) { | ||
const element = program.templateBody | ||
if (element == null) { | ||
return | ||
} | ||
const rootElements = element.children.filter(utils.isVElement) | ||
if (!checkMultiRootNodes && isMultiRootNodes(rootElements)) return | ||
if (attrsRefs.length > 0) { | ||
for (const attrsRef of attrsRefs) { | ||
context.report({ | ||
node: attrsRef.id, | ||
messageId: 'noDuplicateAttrInheritance' | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
) | ||
} | ||
} |
@@ -29,2 +29,5 @@ /** | ||
uniqueItems: true | ||
}, | ||
ignoreElementNamespaces: { | ||
type: 'boolean' | ||
} | ||
@@ -45,2 +48,4 @@ }, | ||
const allow = new Set(options.allow) | ||
/** @type {boolean} */ | ||
const ignoreElementNamespaces = options.ignoreElementNamespaces === true | ||
@@ -67,3 +72,6 @@ /** | ||
const element = node.parent.parent | ||
if (utils.isCustomComponent(element) && !isAllowedComponent(element)) { | ||
if ( | ||
utils.isCustomComponent(element, ignoreElementNamespaces) && | ||
!isAllowedComponent(element) | ||
) { | ||
context.report({ | ||
@@ -70,0 +78,0 @@ node, |
@@ -9,8 +9,40 @@ /** | ||
/** @param expression {Expression | null} */ | ||
function expressionIsRef(expression) { | ||
// @ts-ignore | ||
return expression?.callee?.name === 'ref' | ||
/** | ||
* @typedef ScriptRef | ||
* @type {{node: Expression, ref: string}} | ||
*/ | ||
/** | ||
* @param declarator {VariableDeclarator} | ||
* @returns {ScriptRef} | ||
* */ | ||
function convertDeclaratorToScriptRef(declarator) { | ||
return { | ||
// @ts-ignore | ||
node: declarator.init, | ||
// @ts-ignore | ||
ref: declarator.id.name | ||
} | ||
} | ||
/** | ||
* @param body {(Statement | ModuleDeclaration)[]} | ||
* @returns {ScriptRef[]} | ||
* */ | ||
function getScriptRefsFromSetupFunction(body) { | ||
/** @type {VariableDeclaration[]} */ | ||
const variableDeclarations = body.filter( | ||
(child) => child.type === 'VariableDeclaration' | ||
) | ||
const variableDeclarators = variableDeclarations.map( | ||
(declaration) => declaration.declarations[0] | ||
) | ||
const refDeclarators = variableDeclarators.filter((declarator) => | ||
// @ts-ignore | ||
['ref', 'shallowRef'].includes(declarator.init?.callee?.name) | ||
) | ||
return refDeclarators.map(convertDeclaratorToScriptRef) | ||
} | ||
/** @type {import("eslint").Rule.RuleModule} */ | ||
@@ -22,3 +54,3 @@ module.exports = { | ||
description: | ||
'require using `useTemplateRef` instead of `ref` for template refs', | ||
'require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs', | ||
categories: undefined, | ||
@@ -29,3 +61,3 @@ url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html' | ||
messages: { | ||
preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'." | ||
preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'." | ||
} | ||
@@ -39,7 +71,2 @@ }, | ||
/** | ||
* @typedef ScriptRef | ||
* @type {{node: Expression, ref: string}} | ||
*/ | ||
/** | ||
* @type ScriptRef[] */ | ||
@@ -49,26 +76,24 @@ const scriptRefs = [] | ||
return utils.compositingVisitors( | ||
utils.defineTemplateBodyVisitor( | ||
context, | ||
{ | ||
'VAttribute[directive=false]'(node) { | ||
if (node.key.name === 'ref' && node.value?.value) { | ||
templateRefs.add(node.value.value) | ||
} | ||
utils.defineTemplateBodyVisitor(context, { | ||
'VAttribute[directive=false]'(node) { | ||
if (node.key.name === 'ref' && node.value?.value) { | ||
templateRefs.add(node.value.value) | ||
} | ||
}, | ||
{ | ||
VariableDeclarator(declarator) { | ||
if (!expressionIsRef(declarator.init)) { | ||
return | ||
} | ||
} | ||
}), | ||
utils.defineVueVisitor(context, { | ||
onSetupFunctionEnter(node) { | ||
// @ts-ignore | ||
const newScriptRefs = getScriptRefsFromSetupFunction(node.body.body) | ||
scriptRefs.push({ | ||
// @ts-ignore | ||
node: declarator.init, | ||
// @ts-ignore | ||
ref: declarator.id.name | ||
}) | ||
} | ||
scriptRefs.push(...newScriptRefs) | ||
} | ||
), | ||
}), | ||
utils.defineScriptSetupVisitor(context, { | ||
Program(node) { | ||
const newScriptRefs = getScriptRefsFromSetupFunction(node.body) | ||
scriptRefs.push(...newScriptRefs) | ||
} | ||
}), | ||
{ | ||
@@ -87,3 +112,7 @@ 'Program:exit'() { | ||
node: scriptRef.node, | ||
messageId: 'preferUseTemplateRef' | ||
messageId: 'preferUseTemplateRef', | ||
data: { | ||
// @ts-ignore | ||
name: scriptRef.node?.callee?.name | ||
} | ||
}) | ||
@@ -90,0 +119,0 @@ } |
@@ -101,26 +101,18 @@ /** | ||
utils.defineScriptSetupVisitor(context, { | ||
onDefineSlotsEnter(node) { | ||
const typeArguments = | ||
'typeArguments' in node ? node.typeArguments : node.typeParameters | ||
const param = /** @type {TypeNode|undefined} */ ( | ||
typeArguments?.params[0] | ||
) | ||
if (!param) return | ||
onDefineSlotsEnter(_node, slots) { | ||
for (const slot of slots) { | ||
if (!slot.slotName) { | ||
continue | ||
} | ||
if (param.type === 'TSTypeLiteral') { | ||
for (const memberNode of param.members) { | ||
const slotName = getSlotsName(memberNode) | ||
if (!slotName) continue | ||
if (slotsDefined.has(slotName)) { | ||
context.report({ | ||
node: memberNode, | ||
messageId: 'alreadyDefinedSlot', | ||
data: { | ||
slotName | ||
} | ||
}) | ||
} else { | ||
slotsDefined.add(slotName) | ||
} | ||
if (slotsDefined.has(slot.slotName)) { | ||
context.report({ | ||
node: slot.node, | ||
messageId: 'alreadyDefinedSlot', | ||
data: { | ||
slotName: slot.slotName | ||
} | ||
}) | ||
} else { | ||
slotsDefined.add(slot.slotName) | ||
} | ||
@@ -127,0 +119,0 @@ } |
@@ -5,2 +5,3 @@ 'use strict' | ||
const casing = require('../utils/casing') | ||
const { toRegExp } = require('../utils/regexp') | ||
@@ -39,2 +40,8 @@ module.exports = { | ||
additionalItems: false | ||
}, | ||
ignoreTags: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
uniqueItems: true, | ||
additionalItems: false | ||
} | ||
@@ -61,2 +68,7 @@ }, | ||
const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || [] | ||
/** @type {RegExp[]} */ | ||
const ignoredTagsRegexps = ( | ||
(optionsPayload && optionsPayload.ignoreTags) || | ||
[] | ||
).map(toRegExp) | ||
const autofix = Boolean(optionsPayload && optionsPayload.autofix) | ||
@@ -105,5 +117,16 @@ | ||
/** @param {string} name */ | ||
function isIgnoredTagName(name) { | ||
return ignoredTagsRegexps.some((re) => re.test(name)) | ||
} | ||
return utils.defineTemplateBodyVisitor(context, { | ||
"VAttribute[directive=true][key.name.name='on']"(node) { | ||
if (!utils.isCustomComponent(node.parent.parent)) return | ||
const element = node.parent.parent | ||
if ( | ||
!utils.isCustomComponent(element) || | ||
isIgnoredTagName(element.rawName) | ||
) { | ||
return | ||
} | ||
if (!node.key.argument || node.key.argument.type !== 'VIdentifier') { | ||
@@ -110,0 +133,0 @@ return |
@@ -8,7 +8,9 @@ const { | ||
flattenTypeNodes, | ||
isTSInterfaceBody | ||
isTSInterfaceBody, | ||
extractRuntimeSlots | ||
} = require('./ts-ast') | ||
const { | ||
getComponentPropsFromTypeDefineTypes, | ||
getComponentEmitsFromTypeDefineTypes | ||
getComponentEmitsFromTypeDefineTypes, | ||
getComponentSlotsFromTypeDefineTypes | ||
} = require('./ts-types') | ||
@@ -26,2 +28,5 @@ | ||
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit | ||
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot | ||
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot | ||
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot | ||
*/ | ||
@@ -32,3 +37,4 @@ | ||
getComponentPropsFromTypeDefine, | ||
getComponentEmitsFromTypeDefine | ||
getComponentEmitsFromTypeDefine, | ||
getComponentSlotsFromTypeDefine | ||
} | ||
@@ -92,1 +98,28 @@ | ||
} | ||
/** | ||
* Get all slots by looking at all component's properties | ||
* @param {RuleContext} context The ESLint rule context object. | ||
* @param {TypeNode} slotsNode Type with slots definition | ||
* @return {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots | ||
*/ | ||
function getComponentSlotsFromTypeDefine(context, slotsNode) { | ||
/** @type {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} */ | ||
const result = [] | ||
for (const defNode of flattenTypeNodes( | ||
context, | ||
/** @type {TSESTreeTypeNode} */ (slotsNode) | ||
)) { | ||
if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) { | ||
result.push(...extractRuntimeSlots(defNode)) | ||
} else { | ||
result.push( | ||
...getComponentSlotsFromTypeDefineTypes( | ||
context, | ||
/** @type {TypeNode} */ (defNode) | ||
) | ||
) | ||
} | ||
} | ||
return result | ||
} |
@@ -18,2 +18,4 @@ const { getScope } = require('../scope') | ||
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit | ||
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot | ||
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot | ||
*/ | ||
@@ -30,3 +32,4 @@ | ||
extractRuntimeProps, | ||
extractRuntimeEmits | ||
extractRuntimeEmits, | ||
extractRuntimeSlots | ||
} | ||
@@ -215,2 +218,34 @@ | ||
/** | ||
* @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node | ||
* @returns {IterableIterator<ComponentTypeSlot | ComponentUnknownSlot>} | ||
*/ | ||
function* extractRuntimeSlots(node) { | ||
const members = node.type === 'TSTypeLiteral' ? node.members : node.body | ||
for (const member of members) { | ||
if ( | ||
member.type === 'TSPropertySignature' || | ||
member.type === 'TSMethodSignature' | ||
) { | ||
if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') { | ||
yield { | ||
type: 'unknown', | ||
slotName: null, | ||
node: /** @type {Expression} */ (member.key) | ||
} | ||
continue | ||
} | ||
yield { | ||
type: 'type', | ||
key: /** @type {Identifier | Literal} */ (member.key), | ||
slotName: | ||
member.key.type === 'Identifier' | ||
? member.key.name | ||
: `${member.key.value}`, | ||
node: /** @type {TSPropertySignature | TSMethodSignature} */ (member) | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* @param {TSESTreeParameter} eventName | ||
@@ -217,0 +252,0 @@ * @param {TSCallSignatureDeclaration | TSFunctionType} member |
@@ -27,2 +27,4 @@ const { | ||
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit | ||
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot | ||
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot | ||
*/ | ||
@@ -33,2 +35,3 @@ | ||
getComponentEmitsFromTypeDefineTypes, | ||
getComponentSlotsFromTypeDefineTypes, | ||
inferRuntimeTypeFromTypeNode | ||
@@ -128,3 +131,31 @@ } | ||
/** | ||
* Get all slots by looking at all component's properties | ||
* @param {RuleContext} context The ESLint rule context object. | ||
* @param {TypeNode} slotsNode Type with slots definition | ||
* @return {(ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots | ||
*/ | ||
function getComponentSlotsFromTypeDefineTypes(context, slotsNode) { | ||
const services = getTSParserServices(context) | ||
const tsNode = services && services.tsNodeMap.get(slotsNode) | ||
const type = tsNode && services.checker.getTypeAtLocation(tsNode) | ||
if ( | ||
!type || | ||
isAny(type) || | ||
isUnknown(type) || | ||
isNever(type) || | ||
isNull(type) | ||
) { | ||
return [ | ||
{ | ||
type: 'unknown', | ||
slotName: null, | ||
node: slotsNode | ||
} | ||
] | ||
} | ||
return [...extractRuntimeSlots(type, slotsNode)] | ||
} | ||
/** | ||
* @param {RuleContext} context The ESLint rule context object. | ||
* @param {TypeNode|Expression} node | ||
@@ -267,2 +298,19 @@ * @returns {string[]} | ||
* @param {Type} type | ||
* @param {TypeNode} slotsNode Type with slots definition | ||
* @returns {IterableIterator<ComponentInferTypeSlot>} | ||
*/ | ||
function* extractRuntimeSlots(type, slotsNode) { | ||
for (const property of type.getProperties()) { | ||
const name = property.getName() | ||
yield { | ||
type: 'infer-type', | ||
slotName: name, | ||
node: slotsNode | ||
} | ||
} | ||
} | ||
/** | ||
* @param {Type} type | ||
* @returns {Iterable<Type>} | ||
@@ -269,0 +317,0 @@ */ |
{ | ||
"name": "eslint-plugin-vue", | ||
"version": "9.31.0", | ||
"version": "9.32.0", | ||
"description": "Official ESLint plugin for Vue.js", | ||
@@ -70,3 +70,3 @@ "main": "lib/index.js", | ||
"@ota-meshi/site-kit-eslint-editor-vue": "^0.2.4", | ||
"@stylistic/eslint-plugin": "^2.9.0", | ||
"@stylistic/eslint-plugin": "~2.10.0", | ||
"@types/eslint": "^8.56.2", | ||
@@ -73,0 +73,0 @@ "@types/eslint-visitor-keys": "^3.3.2", |
Sorry, the diff of this file is too big to display
1382278
323
45190