From fbd26766c4eab99adc951a5ea5aa709dd7c20da8 Mon Sep 17 00:00:00 2001 From: ota Date: Wed, 13 May 2020 23:50:53 +0900 Subject: [PATCH] Refactor --- lib/rules/no-async-in-computed-properties.js | 113 +++--- lib/rules/no-deprecated-events-api.js | 25 +- lib/rules/no-lifecycle-after-await.js | 99 ++--- lib/rules/no-setup-props-destructure.js | 159 ++++---- .../no-side-effects-in-computed-properties.js | 99 +++-- lib/rules/no-watch-after-await.js | 99 ++--- lib/rules/require-explicit-emits.js | 266 ++++++------- lib/rules/require-render-return.js | 40 +- lib/rules/return-in-computed-property.js | 38 +- lib/utils/index.js | 368 ++++++++++++------ tests/lib/rules/no-setup-props-destructure.js | 60 +++ tests/lib/rules/require-explicit-emits.js | 14 +- 12 files changed, 725 insertions(+), 655 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 42956550d..35a785b48 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -72,7 +72,7 @@ module.exports = { }, create (context) { - const forbiddenNodes = [] + const computedPropertiesMap = new Map() let scopeStack = { upper: null, body: null } const expressionTypes = { @@ -83,13 +83,9 @@ module.exports = { timed: 'timed function' } - function onFunctionEnter (node) { + function onFunctionEnter (node, { node: vueNode }) { if (node.async) { - forbiddenNodes.push({ - node: node, - type: 'async', - targetBody: node.body - }) + verify(node, node.body, 'async', computedPropertiesMap.get(vueNode)) } scopeStack = { upper: scopeStack, body: node.body } @@ -98,68 +94,53 @@ module.exports = { function onFunctionExit () { scopeStack = scopeStack.upper } - return Object.assign({}, - { - ':function': onFunctionEnter, - ':function:exit': onFunctionExit, - - NewExpression (node) { - if (node.callee.name === 'Promise') { - forbiddenNodes.push({ - node: node, - type: 'new', - targetBody: scopeStack.body - }) - } - }, - - CallExpression (node) { - if (isPromise(node)) { - forbiddenNodes.push({ - node: node, - type: 'promise', - targetBody: scopeStack.body - }) - } else if (isTimedFunction(node)) { - forbiddenNodes.push({ - node: node, - type: 'timed', - targetBody: scopeStack.body - }) - } - }, - - AwaitExpression (node) { - forbiddenNodes.push({ + + function verify (node, targetBody, type, computedProperties) { + computedProperties.forEach(cp => { + if ( + cp.value && + node.loc.start.line >= cp.value.loc.start.line && + node.loc.end.line <= cp.value.loc.end.line && + targetBody === cp.value + ) { + context.report({ node: node, - type: 'await', - targetBody: scopeStack.body - }) - } - }, - utils.executeOnVue(context, (obj) => { - const computedProperties = utils.getComputedProperties(obj) - - computedProperties.forEach(cp => { - forbiddenNodes.forEach(el => { - if ( - cp.value && - el.node.loc.start.line >= cp.value.loc.start.line && - el.node.loc.end.line <= cp.value.loc.end.line && - el.targetBody === cp.value - ) { - context.report({ - node: el.node, - message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.', - data: { - expressionName: expressionTypes[el.type], - propertyName: cp.key - } - }) + message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.', + data: { + expressionName: expressionTypes[type], + propertyName: cp.key } }) - }) + } }) - ) + } + return utils.defineVueVisitor(context, { + ObjectExpression (node, { node: vueNode }) { + if (node !== vueNode) { + return + } + computedPropertiesMap.set(node, utils.getComputedProperties(node)) + }, + ':function': onFunctionEnter, + ':function:exit': onFunctionExit, + + NewExpression (node, { node: vueNode }) { + if (node.callee.name === 'Promise') { + verify(node, scopeStack.body, 'new', computedPropertiesMap.get(vueNode)) + } + }, + + CallExpression (node, { node: vueNode }) { + if (isPromise(node)) { + verify(node, scopeStack.body, 'promise', computedPropertiesMap.get(vueNode)) + } else if (isTimedFunction(node)) { + verify(node, scopeStack.body, 'timed', computedPropertiesMap.get(vueNode)) + } + }, + + AwaitExpression (node, { node: vueNode }) { + verify(node, scopeStack.body, 'await', computedPropertiesMap.get(vueNode)) + } + }) } } diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js index cc21edd40..8537469ee 100644 --- a/lib/rules/no-deprecated-events-api.js +++ b/lib/rules/no-deprecated-events-api.js @@ -30,28 +30,17 @@ module.exports = { }, create (context) { - const forbiddenNodes = [] - - return Object.assign( + return utils.defineVueVisitor(context, { 'CallExpression > MemberExpression > ThisExpression' (node) { if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return - forbiddenNodes.push(node.parent.parent) + + context.report({ + node: node.parent.parent, + messageId: 'noDeprecatedEventsApi' + }) } - }, - utils.executeOnVue(context, (obj) => { - forbiddenNodes.forEach(node => { - if ( - node.loc.start.line >= obj.loc.start.line && - node.loc.end.line <= obj.loc.end.line - ) { - context.report({ - node, - messageId: 'noDeprecatedEventsApi' - }) - } - }) - }) + } ) } } diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js index c4ae51a0e..b1c3c2439 100644 --- a/lib/rules/no-lifecycle-after-await.js +++ b/lib/rules/no-lifecycle-after-await.js @@ -25,16 +25,6 @@ module.exports = { create (context) { const lifecycleHookCallNodes = new Set() const setupFunctions = new Map() - const forbiddenNodes = new Map() - - function addForbiddenNode (property, node) { - let list = forbiddenNodes.get(property) - if (!list) { - list = [] - forbiddenNodes.set(property, list) - } - list.push(node) - } let scopeStack = { upper: null, functionNode: null } @@ -56,56 +46,53 @@ module.exports = { for (const { node } of tracker.iterateEsmReferences(traceMap)) { lifecycleHookCallNodes.add(node) } - }, - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - - setupFunctions.set(node.value, { - setupProperty: node, - afterAwait: false - }) - }, - ':function' (node) { - scopeStack = { upper: scopeStack, functionNode: node } - }, - 'AwaitExpression' () { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData) { - return - } - setupFunctionData.afterAwait = true - }, - 'CallExpression' (node) { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData || !setupFunctionData.afterAwait) { - return - } - - if (lifecycleHookCallNodes.has(node)) { - addForbiddenNode(setupFunctionData.setupProperty, node) - } - }, - ':function:exit' (node) { - scopeStack = scopeStack.upper - - setupFunctions.delete(node) } }, - utils.executeOnVue(context, obj => { - const reportsList = obj.properties - .map(item => forbiddenNodes.get(item)) - .filter(reports => !!reports) - for (const reports of reportsList) { - for (const node of reports) { - context.report({ - node, - messageId: 'forbidden' + utils.defineVueVisitor(context, + { + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { + if (node.parent !== vueNode) { + return + } + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + + setupFunctions.set(node.value, { + setupProperty: node, + afterAwait: false }) + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + 'AwaitExpression' () { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + setupFunctionData.afterAwait = true + }, + 'CallExpression' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData || !setupFunctionData.afterAwait) { + return + } + + if (lifecycleHookCallNodes.has(node)) { + context.report({ + node, + messageId: 'forbidden' + }) + } + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupFunctions.delete(node) } - } - }) + }, + ) ) } } diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index 7246369af..f167357da 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -22,116 +22,93 @@ module.exports = { } }, create (context) { - const setupFunctions = new Map() - const forbiddenNodes = new Map() + const setupScopePropsReferenceIds = new Map() - function addForbiddenNode (property, node, messageId) { - let list = forbiddenNodes.get(property) - if (!list) { - list = [] - forbiddenNodes.set(property, list) - } - list.push({ + function report (node, messageId) { + context.report({ node, messageId }) } - function verify (left, right, { propsReferenceIds, setupProperty }) { + function verify (left, right, propsReferenceIds) { if (!right) { return } - if (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') { - if (propsReferenceIds.has(right)) { - addForbiddenNode(setupProperty, left, 'getProperty') - } - } else if (left.type === 'Identifier' && right.type === 'MemberExpression') { - if (propsReferenceIds.has(right.object)) { - addForbiddenNode(setupProperty, right, 'getProperty') - } + if (left.type !== 'ArrayPattern' && left.type !== 'ObjectPattern' && right.type !== 'MemberExpression') { + return + } + + let rightId = right + while (rightId.type === 'MemberExpression') { + rightId = rightId.object + } + if (propsReferenceIds.has(rightId)) { + report(left, 'getProperty') } } - let scopeStack = { upper: null, functionNode: null } + let scopeStack = null - return Object.assign( - { - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - const param = node.value.params[0] - if (!param) { - // no arguments - return - } - if (param.type === 'RestElement') { - // cannot check - return - } - if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') { - addForbiddenNode(node, param, 'destructuring') - return - } - setupFunctions.set(node.value, { - setupProperty: node, - propsParam: param, - propsReferenceIds: new Set() - }) - }, - ':function' (node) { - scopeStack = { upper: scopeStack, functionNode: node } - }, - ':function > Identifier' (node) { - // find `setup(*props*)` - const setupFunctionData = setupFunctions.get(node.parent) - if (!setupFunctionData || setupFunctionData.propsParam !== node) { - return - } - const variable = findVariable(context.getScope(), node) - if (!variable) { - return - } - const { propsReferenceIds } = setupFunctionData - for (const reference of variable.references) { - if (!reference.isRead()) { - continue - } + return utils.defineVueVisitor(context, { + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { + if (node.parent !== vueNode) { + return + } + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + const propsParam = node.value.params[0] + if (!propsParam) { + // no arguments + return + } + if (propsParam.type === 'RestElement') { + // cannot check + return + } + if (propsParam.type === 'ArrayPattern' || propsParam.type === 'ObjectPattern') { + report(propsParam, 'destructuring') + return + } - propsReferenceIds.add(reference.identifier) - } - }, - 'VariableDeclarator' (node) { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData) { - return - } - verify(node.id, node.init, setupFunctionData) - }, - 'AssignmentExpression' (node) { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData) { - return + const variable = findVariable(context.getScope(), propsParam) + if (!variable) { + return + } + const propsReferenceIds = new Set() + for (const reference of variable.references) { + if (!reference.isRead()) { + continue } - verify(node.left, node.right, setupFunctionData) - }, - ':function:exit' (node) { - scopeStack = scopeStack.upper - setupFunctions.delete(node) + propsReferenceIds.add(reference.identifier) } + setupScopePropsReferenceIds.set(node.value, propsReferenceIds) }, - utils.executeOnVue(context, obj => { - const reportsList = obj.properties - .map(item => forbiddenNodes.get(item)) - .filter(reports => !!reports) - for (const reports of reportsList) { - for (const report of reports) { - context.report(report) - } + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + 'VariableDeclarator' (node) { + const propsReferenceIds = setupScopePropsReferenceIds.get(scopeStack.functionNode) + if (!propsReferenceIds) { + return } - }) - ) + verify(node.id, node.init, propsReferenceIds) + }, + 'AssignmentExpression' (node) { + const propsReferenceIds = setupScopePropsReferenceIds.get(scopeStack.functionNode) + if (!propsReferenceIds) { + return + } + verify(node.left, node.right, propsReferenceIds) + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupScopePropsReferenceIds.delete(node) + } + }) } } diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js index a11c30326..eab6e0fae 100644 --- a/lib/rules/no-side-effects-in-computed-properties.js +++ b/lib/rules/no-side-effects-in-computed-properties.js @@ -23,7 +23,7 @@ module.exports = { }, create (context) { - const forbiddenNodes = [] + const computedPropertiesMap = new Map() let scopeStack = { upper: null, body: null } function onFunctionEnter (node) { @@ -34,63 +34,56 @@ module.exports = { scopeStack = scopeStack.upper } - return Object.assign({}, - { - ':function': onFunctionEnter, - ':function:exit': onFunctionExit, + function verify (node, targetBody, computedProperties) { + computedProperties.forEach(cp => { + if ( + cp.value && + node.loc.start.line >= cp.value.loc.start.line && + node.loc.end.line <= cp.value.loc.end.line && + targetBody === cp.value + ) { + context.report({ + node: node, + message: 'Unexpected side effect in "{{key}}" computed property.', + data: { key: cp.key } + }) + } + }) + } - // this.xxx <=|+=|-=> - 'AssignmentExpression' (node) { - if (node.left.type !== 'MemberExpression') return - if (utils.parseMemberExpression(node.left)[0] === 'this') { - forbiddenNodes.push({ - node, - targetBody: scopeStack.body - }) - } - }, - // this.xxx <++|--> - 'UpdateExpression > MemberExpression' (node) { - if (utils.parseMemberExpression(node)[0] === 'this') { - forbiddenNodes.push({ - node, - targetBody: scopeStack.body - }) - } - }, - // this.xxx.func() - 'CallExpression' (node) { - const code = utils.parseMemberOrCallExpression(node) - const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g + return utils.defineVueVisitor(context, { + ObjectExpression (node, { node: vueNode }) { + if (node !== vueNode) { + return + } + computedPropertiesMap.set(node, utils.getComputedProperties(node)) + }, + ':function': onFunctionEnter, + ':function:exit': onFunctionExit, - if (MUTATION_REGEX.test(code)) { - forbiddenNodes.push({ - node, - targetBody: scopeStack.body - }) - } + // this.xxx <=|+=|-=> + 'AssignmentExpression' (node, { node: vueNode }) { + if (node.left.type !== 'MemberExpression') return + if (utils.parseMemberExpression(node.left)[0] === 'this') { + verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) } }, - utils.executeOnVue(context, (obj) => { - const computedProperties = utils.getComputedProperties(obj) + // this.xxx <++|--> + 'UpdateExpression > MemberExpression' (node, { node: vueNode }) { + if (utils.parseMemberExpression(node)[0] === 'this') { + verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) + } + }, + // this.xxx.func() + 'CallExpression' (node, { node: vueNode }) { + const code = utils.parseMemberOrCallExpression(node) + const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g - computedProperties.forEach(cp => { - forbiddenNodes.forEach(({ node, targetBody }) => { - if ( - cp.value && - node.loc.start.line >= cp.value.loc.start.line && - node.loc.end.line <= cp.value.loc.end.line && - targetBody === cp.value - ) { - context.report({ - node: node, - message: 'Unexpected side effect in "{{key}}" computed property.', - data: { key: cp.key } - }) - } - }) - }) - }) + if (MUTATION_REGEX.test(code)) { + verify(node, scopeStack.body, computedPropertiesMap.get(vueNode)) + } + } + } ) } } diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index f65a459de..e666b8616 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -50,16 +50,6 @@ module.exports = { create (context) { const watchCallNodes = new Set() const setupFunctions = new Map() - const forbiddenNodes = new Map() - - function addForbiddenNode (property, node) { - let list = forbiddenNodes.get(property) - if (!list) { - list = [] - forbiddenNodes.set(property, list) - } - list.push(node) - } let scopeStack = { upper: null, functionNode: null } @@ -82,56 +72,53 @@ module.exports = { for (const { node } of tracker.iterateEsmReferences(traceMap)) { watchCallNodes.add(node) } - }, - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - - setupFunctions.set(node.value, { - setupProperty: node, - afterAwait: false - }) - }, - ':function' (node) { - scopeStack = { upper: scopeStack, functionNode: node } - }, - 'AwaitExpression' () { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData) { - return - } - setupFunctionData.afterAwait = true - }, - 'CallExpression' (node) { - const setupFunctionData = setupFunctions.get(scopeStack.functionNode) - if (!setupFunctionData || !setupFunctionData.afterAwait) { - return - } - - if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) { - addForbiddenNode(setupFunctionData.setupProperty, node) - } - }, - ':function:exit' (node) { - scopeStack = scopeStack.upper - - setupFunctions.delete(node) } }, - utils.executeOnVue(context, obj => { - const reportsList = obj.properties - .map(item => forbiddenNodes.get(item)) - .filter(reports => !!reports) - for (const reports of reportsList) { - for (const node of reports) { - context.report({ - node, - messageId: 'forbidden' + utils.defineVueVisitor(context, + { + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, { node: vueNode }) { + if (node.parent !== vueNode) { + return + } + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + + setupFunctions.set(node.value, { + setupProperty: node, + afterAwait: false }) + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + 'AwaitExpression' () { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + setupFunctionData.afterAwait = true + }, + 'CallExpression' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData || !setupFunctionData.afterAwait) { + return + } + + if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) { + context.report({ + node, + messageId: 'forbidden' + }) + } + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupFunctions.delete(node) } - } - }) + }, + ) ) } } diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js index aaca2cebc..2addf8471 100644 --- a/lib/rules/require-explicit-emits.js +++ b/lib/rules/require-explicit-emits.js @@ -100,37 +100,33 @@ module.exports = { create (context) { /** @typedef { { node: Literal, name: string } } EmitCellName */ - const setupFunctions = new Map() - /** @type { Map } */ - const setupEmitCellNames = new Map() - /** @type {Set} */ - const objectEmitCellNames = new Set() + const setupContexts = new Map() + const vueEmitsDeclarations = new Map() + /** @type {EmitCellName[]} */ const templateEmitCellNames = [] /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression, emits: (ComponentArrayEmit | ComponentObjectEmit)[] } | null } */ let vueObjectData = null - function addSetupEmitCellName (property, nameLiteralNode) { - let list = setupEmitCellNames.get(property) - if (!list) { - list = [] - setupEmitCellNames.set(property, list) - } - list.push({ - node: nameLiteralNode, - name: nameLiteralNode.value - }) - } - function addObjectEmitCellName (nameLiteralNode) { - objectEmitCellNames.add({ + function addTemplateEmitCellName (nameLiteralNode) { + templateEmitCellNames.push({ node: nameLiteralNode, name: nameLiteralNode.value }) } - function addTemplateEmitCellName (nameLiteralNode) { - templateEmitCellNames.push({ + + function verify (emitsDeclarations, nameLiteralNode, vueObjectNode) { + const name = nameLiteralNode.value + if (emitsDeclarations.some(e => e.emitName === name)) { + return + } + context.report({ node: nameLiteralNode, - name: nameLiteralNode.value + messageId: 'missing', + data: { + name + }, + suggest: buildSuggest(vueObjectNode, emitsDeclarations, nameLiteralNode, context) }) } @@ -168,161 +164,127 @@ module.exports = { } } }, - Object.assign( - { - 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { - if (utils.getStaticPropertyName(node) !== 'setup') { - return - } - const param = node.value.params[1] - if (!param) { - // no arguments - return - } - if (param.type === 'RestElement') { - // cannot check - return - } - if (param.type === 'ArrayPattern') { - // cannot check + utils.defineVueVisitor(context, { + ObjectExpression (node, { node: vueNode }) { + if (node !== vueNode) { + return + } + vueEmitsDeclarations.set(node, utils.getComponentEmits(node)) + + const setupProperty = node.properties.find(p => utils.getStaticPropertyName(p) === 'setup') + if (!setupProperty) { + return + } + if (!/^(Arrow)?FunctionExpression$/.test(setupProperty.value.type)) { + return + } + const contextParam = setupProperty.value.params[1] + if (!contextParam) { + // no arguments + return + } + if (contextParam.type === 'RestElement') { + // cannot check + return + } + if (contextParam.type === 'ArrayPattern') { + // cannot check + return + } + const contextReferenceIds = new Set() + const emitReferenceIds = new Set() + if (contextParam.type === 'ObjectPattern') { + const emitProperty = contextParam.properties.find(p => p.type === 'Property' && utils.getStaticPropertyName(p) === 'emit') + if (!emitProperty) { return } - let emitParam - if (param.type === 'ObjectPattern') { - const emitProperty = param.properties.find(p => p.type === 'Property' && utils.getStaticPropertyName(p) === 'emit') - if (!emitProperty) { - return - } - emitParam = emitProperty.value - } - setupFunctions.set(node.value, { - setupProperty: node, - contextParam: param, - emitParam, - contextReferenceIds: new Set(), - emitReferenceIds: new Set() - }) - }, - ':function > Identifier, :function > ObjectPattern' (node) { - // find `setup(props, *context*)` - const setupFunctionData = setupFunctions.get(node.parent) - if (!setupFunctionData || setupFunctionData.contextParam !== node) { + const emitParam = emitProperty.value + // `setup(props, {emit})` + const variable = findVariable(context.getScope(), emitParam) + if (!variable) { return } - if (node.type === 'Identifier') { - // `setup(props, context)` - const variable = findVariable(context.getScope(), node) - if (!variable) { - return + for (const reference of variable.references) { + if (!reference.isRead()) { + continue } - const { contextReferenceIds } = setupFunctionData - for (const reference of variable.references) { - if (!reference.isRead()) { - continue - } - contextReferenceIds.add(reference.identifier) - } - } else if (node.type === 'ObjectPattern') { - // `setup(props, {emit})` - const variable = findVariable(context.getScope(), setupFunctionData.emitParam) - if (!variable) { - return - } - const { emitReferenceIds } = setupFunctionData - for (const reference of variable.references) { - if (!reference.isRead()) { - continue - } - - emitReferenceIds.add(reference.identifier) - } + emitReferenceIds.add(reference.identifier) } - }, - 'CallExpression[arguments.0.type=Literal]' (node) { - const callee = node.callee - const nameLiteralNode = node.arguments[0] - if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') { - // cannot check + } else { + // `setup(props, context)` + const variable = findVariable(context.getScope(), contextParam) + if (!variable) { return } - let emit - if (callee.type === 'MemberExpression') { - const name = utils.getStaticPropertyName(callee) - if (name === 'emit' || name === '$emit') { - emit = { name, member: callee } - } - } - - // find setup context - for (const { contextReferenceIds, emitReferenceIds, setupProperty } of setupFunctions.values()) { - if (emitReferenceIds.has(callee)) { - addSetupEmitCellName(setupProperty, nameLiteralNode) - } - if (emit && emit.name === 'emit' && contextReferenceIds.has(emit.member.object)) { - addSetupEmitCellName(setupProperty, nameLiteralNode) + for (const reference of variable.references) { + if (!reference.isRead()) { + continue } - } - // find $emit - if (emit && emit.name === '$emit') { - const objectType = emit.member.object.type - if (objectType === 'Identifier' || objectType === 'ThisExpression') { - addObjectEmitCellName(nameLiteralNode) - } + contextReferenceIds.add(reference.identifier) } - }, - ':function:exit' (node) { - setupFunctions.delete(node) } + setupContexts.set(node, { + contextReferenceIds, + emitReferenceIds + }) }, - utils.executeOnVue(context, (obj, type) => { - const emitsDeclarations = utils.getComponentEmits(obj) - if (!vueObjectData || vueObjectData.type !== 'export') { - vueObjectData = { - type, - object: obj, - emits: emitsDeclarations - } + 'CallExpression[arguments.0.type=Literal]' (node, { node: vueNode }) { + const callee = node.callee + const nameLiteralNode = node.arguments[0] + if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') { + // cannot check + return } + const emitsDeclarations = vueEmitsDeclarations.get(vueNode) - const emitsDeclarationNames = new Set(emitsDeclarations.map(e => e.emitName)) - - const componentEmitCellNames = [] - - // extract this.$emit() - for (const emit of [...objectEmitCellNames]) { - if (obj.range[0] <= emit.node.range[0] && emit.node.range[1] <= obj.range[1]) { - componentEmitCellNames.push(emit) - objectEmitCellNames.delete(emit)// verified + let emit + if (callee.type === 'MemberExpression') { + const name = utils.getStaticPropertyName(callee) + if (name === 'emit' || name === '$emit') { + emit = { name, member: callee } } } - // extract setup(props,context) {context.emit()} - for (const prop of obj.properties) { - const setupEmits = setupEmitCellNames.get(prop) - if (!setupEmits) { - continue + // verify setup context + const setupContext = setupContexts.get(vueNode) + if (setupContext) { + const { contextReferenceIds, emitReferenceIds } = setupContext + if (emitReferenceIds.has(callee)) { + // verify setup(props,{emit}) {emit()} + verify(emitsDeclarations, nameLiteralNode, vueNode) + } else if (emit && emit.name === 'emit' && contextReferenceIds.has(emit.member.object)) { + // verify setup(props,context) {context.emit()} + verify(emitsDeclarations, nameLiteralNode, vueNode) } - componentEmitCellNames.push(...setupEmits) - setupEmitCellNames.delete(prop)// verified } - for (const { name, node } of componentEmitCellNames) { - if (emitsDeclarationNames.has(name)) { - continue + // verify $emit + if (emit && emit.name === '$emit') { + const objectType = emit.member.object.type + if (objectType === 'Identifier' || objectType === 'ThisExpression') { + // verify this.$emit() + verify(emitsDeclarations, nameLiteralNode, vueNode) } - context.report({ - node, - messageId: 'missing', - data: { - name - }, - suggest: buildSuggest(obj, emitsDeclarations, node, context) - }) } - }) - )) + }, + 'ObjectExpression:exit' (node, { node: vueNode, type }) { + if (node !== vueNode) { + return + } + if (!vueObjectData || vueObjectData.type !== 'export') { + vueObjectData = { + type, + object: node, + emits: vueEmitsDeclarations.get(node) + } + } + setupContexts.delete(node) + vueEmitsDeclarations.delete(node) + } + }), + ) } } diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index 3ae1623d2..17cfcb355 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -23,32 +23,36 @@ module.exports = { }, create (context) { - const forbiddenNodes = [] + const renderFunctions = new Map() // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- return Object.assign({}, + utils.defineVueVisitor(context, + { + ObjectExpression (obj, { node: vueNode }) { + if (obj !== vueNode) { + return + } + const node = obj.properties.find(item => item.type === 'Property' && + utils.getStaticPropertyName(item) === 'render' && + (item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression') + ) + if (!node) return + renderFunctions.set(node.value, node.key) + } + } + ), utils.executeOnFunctionsWithoutReturn(true, node => { - forbiddenNodes.push(node) + if (renderFunctions.has(node)) { + context.report({ + node: renderFunctions.get(node), + message: 'Expected to return a value in render function.' + }) + } }), - utils.executeOnVue(context, obj => { - const node = obj.properties.find(item => item.type === 'Property' && - utils.getStaticPropertyName(item) === 'render' && - (item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression') - ) - if (!node) return - - forbiddenNodes.forEach(el => { - if (node.value === el) { - context.report({ - node: node.key, - message: 'Expected to return a value in render function.' - }) - } - }) - }) ) } } diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js index 00a43a5c2..45ead3060 100644 --- a/lib/rules/return-in-computed-property.js +++ b/lib/rules/return-in-computed-property.js @@ -36,6 +36,7 @@ module.exports = { const options = context.options[0] || {} const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false) + const computedProperties = new Set() const forbiddenNodes = [] // ---------------------------------------------------------------------- @@ -43,26 +44,33 @@ module.exports = { // ---------------------------------------------------------------------- return Object.assign({}, + utils.defineVueVisitor(context, + { + ObjectExpression (obj, { node: vueNode }) { + if (obj !== vueNode) { + return + } + for (const computedProperty of utils.getComputedProperties(obj)) { + computedProperties.add(computedProperty) + } + } + } + ), utils.executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, node => { forbiddenNodes.push(node) - }), - utils.executeOnVue(context, properties => { - const computedProperties = utils.getComputedProperties(properties) computedProperties.forEach(cp => { - forbiddenNodes.forEach(el => { - if (cp.value && cp.value.parent === el) { - context.report({ - node: el, - message: 'Expected to return a value in "{{name}}" computed property.', - data: { - name: cp.key - } - }) - } - }) + if (cp.value && cp.value.parent === node) { + context.report({ + node, + message: 'Expected to return a value in "{{name}}" computed property.', + data: { + name: cp.key + } + }) + } }) - }) + }), ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index 27b64cdfc..3e1ce4c58 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -17,6 +17,11 @@ * @typedef {import('vue-eslint-parser').AST.ESLintTemplateLiteral} TemplateLiteral */ +/** + * @typedef {import('eslint').Rule.RuleContext} RuleContext + * @typedef {import('vue-eslint-parser').AST.Token} Token + */ + /** * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], propName: string} } ComponentArrayProp * @typedef { {key: Property['key'], value: Property['value'], node: Property, propName: string} } ComponentObjectProp @@ -37,6 +42,11 @@ const assert = require('assert') const path = require('path') const vueEslintParser = require('vue-eslint-parser') +/** + * @type { WeakMap } + */ +const componentComments = new WeakMap() + /** * Wrap the rule context object to override methods which access to tokens (such as getTokenAfter). * @param {RuleContext} context The rule context object. @@ -521,115 +531,66 @@ module.exports = { }) }, - isVueFile (path) { - return path.endsWith('.vue') || path.endsWith('.jsx') - }, + isVueFile, /** - * Check whether the given node is a Vue component based - * on the filename and default export type - * export default {} in .vue || .jsx - * @param {ASTNode} node Node to check - * @param {string} path File name with extension - * @returns {boolean} + * Check if current file is a Vue instance or component and call callback + * @param {RuleContext} context The ESLint rule context object. + * @param {Function} cb Callback function */ - isVueComponentFile (node, path) { - return this.isVueFile(path) && - node.type === 'ExportDefaultDeclaration' && - node.declaration.type === 'ObjectExpression' + executeOnVue (context, cb) { + return compositingVisitors( + this.executeOnVueComponent(context, cb), + this.executeOnVueInstance(context, cb) + ) }, /** - * Check whether given node is Vue component - * Vue.component('xxx', {}) || component('xxx', {}) - * @param {ASTNode} node Node to check - * @returns {boolean} + * Define handlers to traverse the Vue Objects. + * @param {RuleContext} context The ESLint rule context object. + * @param {Object} visitor The visitor to traverse the Vue Objects. + * @param {Function} cb Callback function */ - isVueComponent (node) { - if (node.type === 'CallExpression') { - const callee = node.callee - - if (callee.type === 'MemberExpression') { - const calleeObject = unwrapTypes(callee.object) - - if (calleeObject.type === 'Identifier') { - const propName = getStaticPropertyName(callee.property) - if (calleeObject.name === 'Vue') { - // for Vue.js 2.x - // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {}) - const isFullVueComponentForVue2 = - ['component', 'mixin', 'extend'].includes(propName) && - isObjectArgument(node) - - return isFullVueComponentForVue2 - } + defineVueVisitor (context, visitor) { + let vueStack = null - // for Vue.js 3.x - // app.component('xxx', {}) || app.mixin({}) - const isFullVueComponent = - ['component', 'mixin'].includes(propName) && - isObjectArgument(node) - - return isFullVueComponent - } + function callVisitor (key, node) { + if (visitor[key] && vueStack) { + visitor[key](node, vueStack) } - - if (callee.type === 'Identifier') { - if (callee.name === 'component') { - // for Vue.js 2.x - // component('xxx', {}) - const isDestructedVueComponent = isObjectArgument(node) - return isDestructedVueComponent - } - if (callee.name === 'createApp') { - // for Vue.js 3.x - // createApp({}) - const isAppVueComponent = isObjectArgument(node) - return isAppVueComponent - } - if (callee.name === 'defineComponent') { - // for Vue.js 3.x - // defineComponent({}) - const isDestructedVueComponent = isObjectArgument(node) - return isDestructedVueComponent - } + } + function objectEnter (node) { + const type = getVueObjectType(context, node) + if (type) { + vueStack = { node, type, parent: vueStack } + } + } + function objectExit (node) { + if (vueStack && vueStack.node === node) { + vueStack = vueStack.parent } } - return false - - function isObjectArgument (node) { - return node.arguments.length > 0 && - unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression' + const vueVisitor = {} + for (const key in visitor) { + vueVisitor[key] = (node) => callVisitor(key, node) } - }, - /** - * Check whether given node is new Vue instance - * new Vue({}) - * @param {ASTNode} node Node to check - * @returns {boolean} - */ - isVueInstance (node) { - const callee = node.callee - return node.type === 'NewExpression' && - callee.type === 'Identifier' && - callee.name === 'Vue' && - node.arguments.length && - unwrapTypes(node.arguments[0]).type === 'ObjectExpression' + return { + ...vueVisitor, + ObjectExpression: vueVisitor.ObjectExpression ? (node) => { + objectEnter(node) + vueVisitor.ObjectExpression(node) + } : objectEnter, + 'ObjectExpression:exit': vueVisitor['ObjectExpression:exit'] ? (node) => { + vueVisitor['ObjectExpression:exit'](node) + objectExit(node) + } : objectExit + } }, - /** - * Check if current file is a Vue instance or component and call callback - * @param {RuleContext} context The ESLint rule context object. - * @param {Function} cb Callback function - */ - executeOnVue (context, cb) { - return Object.assign( - this.executeOnVueComponent(context, cb), - this.executeOnVueInstance(context, cb) - ) - }, + getVueObjectType, + compositingVisitors, /** * Check if current file is a Vue instance (new Vue) and call callback @@ -637,13 +598,11 @@ module.exports = { * @param {Function} cb Callback function */ executeOnVueInstance (context, cb) { - const _this = this - return { - 'NewExpression:exit' (node) { - // new Vue({}) - if (!_this.isVueInstance(node)) return - cb(node.arguments[0], 'instance') + 'ObjectExpression:exit' (node) { + const type = getVueObjectType(context, node) + if (!type || type !== 'instance') return + cb(node, type) } } }, @@ -654,32 +613,11 @@ module.exports = { * @param {Function} cb Callback function */ executeOnVueComponent (context, cb) { - const filePath = context.getFilename() - const sourceCode = context.getSourceCode() - const _this = this - const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value)) - const foundNodes = [] - - const isDuplicateNode = (node) => { - if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true - foundNodes.push(node) - return false - } - return { 'ObjectExpression:exit' (node) { - if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return - cb(node, 'mark') - }, - 'ExportDefaultDeclaration:exit' (node) { - // export default {} in .vue || .jsx - if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return - cb(node.declaration, 'export') - }, - 'CallExpression:exit' (node) { - // Vue.component('xxx', {}) || component('xxx', {}) - if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return - cb(unwrapTypes(node.arguments.slice(-1)[0]), 'definition') + const type = getVueObjectType(context, node) + if (!type || (type !== 'mark' && type !== 'export' && type !== 'definition')) return + cb(node, type) } } }, @@ -919,14 +857,15 @@ module.exports = { */ unwrapTypes } + /** -* Unwrap typescript types like "X as F" -* @template T -* @param {T} node -* @return {T} -*/ + * Unwrap typescript types like "X as F" + * @template T + * @param {T} node + * @return {T} + */ function unwrapTypes (node) { - return node.type === 'TSAsExpression' ? node.expression : node + return !node ? node : node.type === 'TSAsExpression' ? unwrapTypes(node.expression) : node } /** @@ -970,3 +909,174 @@ function getStaticPropertyName (node) { return null } + +function isVueFile (path) { + return path.endsWith('.vue') || path.endsWith('.jsx') +} + +/** + * Check whether the given node is a Vue component based + * on the filename and default export type + * export default {} in .vue || .jsx + * @param {ASTNode} node Node to check + * @param {string} path File name with extension + * @returns {boolean} + */ +function isVueComponentFile (node, path) { + return isVueFile(path) && + node.type === 'ExportDefaultDeclaration' && + node.declaration.type === 'ObjectExpression' +} + +/** + * Check whether given node is Vue component + * Vue.component('xxx', {}) || component('xxx', {}) + * @param {ASTNode} node Node to check + * @returns {boolean} + */ +function isVueComponent (node) { + if (node.type === 'CallExpression') { + const callee = node.callee + + if (callee.type === 'MemberExpression') { + const calleeObject = unwrapTypes(callee.object) + + if (calleeObject.type === 'Identifier') { + const propName = getStaticPropertyName(callee.property) + if (calleeObject.name === 'Vue') { + // for Vue.js 2.x + // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {}) + const isFullVueComponentForVue2 = + ['component', 'mixin', 'extend'].includes(propName) && + isObjectArgument(node) + + return isFullVueComponentForVue2 + } + + // for Vue.js 3.x + // app.component('xxx', {}) || app.mixin({}) + const isFullVueComponent = + ['component', 'mixin'].includes(propName) && + isObjectArgument(node) + + return isFullVueComponent + } + } + + if (callee.type === 'Identifier') { + if (callee.name === 'component') { + // for Vue.js 2.x + // component('xxx', {}) + const isDestructedVueComponent = isObjectArgument(node) + return isDestructedVueComponent + } + if (callee.name === 'createApp') { + // for Vue.js 3.x + // createApp({}) + const isAppVueComponent = isObjectArgument(node) + return isAppVueComponent + } + if (callee.name === 'defineComponent') { + // for Vue.js 3.x + // defineComponent({}) + const isDestructedVueComponent = isObjectArgument(node) + return isDestructedVueComponent + } + } + } + + return false + + function isObjectArgument (node) { + return node.arguments.length > 0 && + unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression' + } +} + +/** + * Check whether given node is new Vue instance + * new Vue({}) + * @param {ASTNode} node Node to check + * @returns {boolean} + */ +function isVueInstance (node) { + const callee = node.callee + return node.type === 'NewExpression' && + callee.type === 'Identifier' && + callee.name === 'Vue' && + node.arguments.length && + unwrapTypes(node.arguments[0]).type === 'ObjectExpression' +} + +/** + * If the given object is a Vue component or instance, returns the Vue definition type. + * @param {RuleContext} context The ESLint rule context object. + * @param {ObjectExpression} node Node to check + * @returns { 'mark' | 'export' | 'definition' | 'instance' | null } The Vue definition type. + */ +function getVueObjectType (context, node) { + if (node.type !== 'ObjectExpression') { + return null + } + let parent = node.parent + while (parent && parent.type === 'TSAsExpression') { + parent = parent.parent + } + if (parent) { + if (parent.type === 'ExportDefaultDeclaration') { + // export default {} in .vue || .jsx + const filePath = context.getFilename() + if (isVueComponentFile(parent, filePath) && unwrapTypes(parent.declaration) === node) { + return 'export' + } + } else if (parent.type === 'CallExpression') { + // Vue.component('xxx', {}) || component('xxx', {}) + if (isVueComponent(parent) && unwrapTypes(parent.arguments.slice(-1)[0]) === node) { + return 'definition' + } + } else if (parent.type === 'NewExpression') { + // new Vue({}) + if (isVueInstance(parent) && unwrapTypes(parent.arguments[0]) === node) { + return 'instance' + } + } + } + if (getComponentComments(context).some(el => el.loc.end.line === node.loc.start.line - 1)) { + return 'mark' + } + return null +} + +/** + * Gets the component comments of a given context. + * @param {RuleContext} context The ESLint rule context object. + * @return {Token[]} The the component comments. + */ +function getComponentComments (context) { + let tokens = componentComments.get(context) + if (tokens) { + return tokens + } + const sourceCode = context.getSourceCode() + tokens = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value)) + componentComments.set(context, tokens) + return tokens +} + +function compositingVisitors (...visitors) { + const visitor = {} + for (const v of visitors) { + for (const key in v) { + if (visitor[key]) { + const o = visitor[key] + visitor[key] = (node) => { + o(node) + v[key](node) + } + } else { + visitor[key] = v[key] + } + } + } + return visitor +} diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js index 2f3bccc51..e81d680fc 100644 --- a/tests/lib/rules/no-setup-props-destructure.js +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -135,6 +135,30 @@ tester.run('no-setup-props-destructure', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -330,6 +354,42 @@ tester.run('no-setup-props-destructure', rule, { line: 6 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] } ] }) diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index ee0e595bd..0a084ab43 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -302,7 +302,6 @@ tester.run('require-explicit-emits', rule, { ` }, - { filename: 'test.vue', code: ` @@ -347,6 +346,19 @@ tester.run('require-explicit-emits', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [