diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js index 5db15a875..6d215edd8 100644 --- a/lib/rules/custom-event-name-casing.js +++ b/lib/rules/custom-event-name-casing.js @@ -24,23 +24,27 @@ const { toRegExp } = require('../utils/regexp') const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase'] const DEFAULT_CASE = 'camelCase' +/** + * @typedef {object} NameWithLoc + * @property {string} name + * @property {SourceLocation} loc + */ /** * Get the name param node from the given CallExpression * @param {CallExpression} node CallExpression - * @returns { Literal & { value: string } | null } + * @returns { NameWithLoc | null } */ function getNameParamNode(node) { const nameLiteralNode = node.arguments[0] - if ( - !nameLiteralNode || - nameLiteralNode.type !== 'Literal' || - typeof nameLiteralNode.value !== 'string' - ) { - // cannot check - return null + if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) { + const name = utils.getStringLiteralValue(nameLiteralNode) + if (name != null) { + return { name, loc: nameLiteralNode.loc } + } } - return /** @type {Literal & { value: string }} */ (nameLiteralNode) + // cannot check + return null } /** * Get the callee member node from the given CallExpression @@ -130,15 +134,15 @@ module.exports = { } /** - * @param { Literal & { value: string } } nameLiteralNode + * @param { NameWithLoc } nameWithLoc */ - function verify(nameLiteralNode) { - const name = nameLiteralNode.value + function verify(nameWithLoc) { + const name = nameWithLoc.name if (isValidEventName(name) || ignores.some((re) => re.test(name))) { return } context.report({ - node: nameLiteralNode, + loc: nameWithLoc.loc, messageId: 'unexpected', data: { name, @@ -155,8 +159,8 @@ module.exports = { * @param {VueObjectData} [info] */ CallExpression(node, info) { - const nameLiteralNode = getNameParamNode(node) - if (!nameLiteralNode) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } @@ -170,7 +174,7 @@ module.exports = { emitReferenceIds.has(node.callee) ) { // verify setup(props,{emit}) {emit()} - verify(nameLiteralNode) + verify(nameWithLoc) } else { const emit = getCalleeMemberNode(node) if ( @@ -180,7 +184,7 @@ module.exports = { contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} - verify(nameLiteralNode) + verify(nameWithLoc) } } } @@ -192,13 +196,13 @@ module.exports = { { CallExpression(node) { const callee = node.callee - const nameLiteralNode = getNameParamNode(node) - if (!nameLiteralNode) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } if (callee.type === 'Identifier' && callee.name === '$emit') { - verify(nameLiteralNode) + verify(nameWithLoc) } } }, diff --git a/lib/rules/no-restricted-custom-event.js b/lib/rules/no-restricted-custom-event.js index 22d128cd8..7987250b4 100644 --- a/lib/rules/no-restricted-custom-event.js +++ b/lib/rules/no-restricted-custom-event.js @@ -56,23 +56,28 @@ function parseOption(option) { return parsed } +/** + * @typedef {object} NameWithLoc + * @property {string} name + * @property {SourceLocation} loc + * @property {Range} range + */ /** * Get the name param node from the given CallExpression * @param {CallExpression} node CallExpression - * @returns { Literal & { value: string } | null } + * @returns { NameWithLoc | null } */ function getNameParamNode(node) { const nameLiteralNode = node.arguments[0] - if ( - !nameLiteralNode || - nameLiteralNode.type !== 'Literal' || - typeof nameLiteralNode.value !== 'string' - ) { - // cannot check - return null + if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) { + const name = utils.getStringLiteralValue(nameLiteralNode) + if (name != null) { + return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range } + } } - return /** @type {Literal & { value: string }} */ (nameLiteralNode) + // cannot check + return null } /** * Get the callee member node from the given CallExpression @@ -134,17 +139,17 @@ module.exports = { const options = context.options.map(parseOption) /** - * @param { Literal & { value: string } } nameLiteralNode + * @param { NameWithLoc } nameWithLoc */ - function verify(nameLiteralNode) { - const name = nameLiteralNode.value + function verify(nameWithLoc) { + const name = nameWithLoc.name for (const option of options) { if (option.test(name)) { const message = option.message || `Using \`${name}\` event is not allowed.` context.report({ - node: nameLiteralNode, + loc: nameWithLoc.loc, messageId: 'restrictedEvent', data: { message }, suggest: option.suggest @@ -152,14 +157,14 @@ module.exports = { { fix(fixer) { const sourceCode = context.getSourceCode() - return fixer.replaceText( - nameLiteralNode, + return fixer.replaceTextRange( + nameWithLoc.range, `${ - sourceCode.text[nameLiteralNode.range[0]] + sourceCode.text[nameWithLoc.range[0]] }${JSON.stringify(option.suggest) .slice(1, -1) .replace(/'/gu, "\\'")}${ - sourceCode.text[nameLiteralNode.range[1] - 1] + sourceCode.text[nameWithLoc.range[1] - 1] }` ) }, @@ -179,13 +184,13 @@ module.exports = { { CallExpression(node) { const callee = node.callee - const nameLiteralNode = getNameParamNode(node) - if (!nameLiteralNode) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } if (callee.type === 'Identifier' && callee.name === '$emit') { - verify(nameLiteralNode) + verify(nameWithLoc) } } }, @@ -239,8 +244,8 @@ module.exports = { }) }, CallExpression(node, { node: vueNode }) { - const nameLiteralNode = getNameParamNode(node) - if (!nameLiteralNode) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } @@ -254,7 +259,7 @@ module.exports = { emitReferenceIds.has(node.callee) ) { // verify setup(props,{emit}) {emit()} - verify(nameLiteralNode) + verify(nameWithLoc) } else { const emit = getCalleeMemberNode(node) if ( @@ -264,7 +269,7 @@ module.exports = { contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} - verify(nameLiteralNode) + verify(nameWithLoc) } } } @@ -275,8 +280,8 @@ module.exports = { }), { CallExpression(node) { - const nameLiteralNode = getNameParamNode(node) - if (!nameLiteralNode) { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } @@ -284,7 +289,7 @@ module.exports = { // verify $emit if (emit && emit.name === '$emit') { // verify this.$emit() - verify(nameLiteralNode) + verify(nameWithLoc) } } } diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js index 1a69c982a..9f531ccba 100644 --- a/lib/rules/require-explicit-emits.js +++ b/lib/rules/require-explicit-emits.js @@ -54,6 +54,30 @@ const FIX_EMITS_AFTER_OPTIONS = new Set([ 'renderTriggered', 'errorCaptured' ]) + +/** + * @typedef {object} NameWithLoc + * @property {string} name + * @property {SourceLocation} loc + * @property {Range} range + */ +/** + * Get the name param node from the given CallExpression + * @param {CallExpression} node CallExpression + * @returns { NameWithLoc | null } + */ +function getNameParamNode(node) { + const nameLiteralNode = node.arguments[0] + if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) { + const name = utils.getStringLiteralValue(nameLiteralNode) + if (name != null) { + return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range } + } + } + + // cannot check + return null +} // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -114,11 +138,11 @@ module.exports = { /** * @param {ComponentEmit[]} emits * @param {ComponentProp[]} props - * @param {Literal} nameLiteralNode + * @param {NameWithLoc} nameWithLoc * @param {ObjectExpression | Program} vueDefineNode */ - function verifyEmit(emits, props, nameLiteralNode, vueDefineNode) { - const name = `${nameLiteralNode.value}` + function verifyEmit(emits, props, nameWithLoc, vueDefineNode) { + const name = nameWithLoc.name if (emits.some((e) => e.emitName === name || e.emitName == null)) { return } @@ -129,7 +153,7 @@ module.exports = { } } context.report({ - node: nameLiteralNode, + loc: nameWithLoc.loc, messageId: 'missing', data: { name, @@ -138,7 +162,7 @@ module.exports = { ? '`emits` option' : '`defineEmits`' }, - suggest: buildSuggest(vueDefineNode, emits, nameLiteralNode, context) + suggest: buildSuggest(vueDefineNode, emits, nameWithLoc, context) }) } @@ -155,13 +179,13 @@ module.exports = { const callVisitor = { /** - * @param {CallExpression & { arguments: [Literal, ...Expression] }} node + * @param {CallExpression} node * @param {VueObjectData} [info] */ - 'CallExpression[arguments.0.type=Literal]'(node, info) { + CallExpression(node, info) { const callee = utils.skipChainExpression(node.callee) - const nameLiteralNode = node.arguments[0] - if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } @@ -188,7 +212,7 @@ module.exports = { verifyEmit( emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], - nameLiteralNode, + nameWithLoc, vueDefineNode ) } else if (emit && emit.name === 'emit') { @@ -201,7 +225,7 @@ module.exports = { verifyEmit( emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], - nameLiteralNode, + nameWithLoc, vueDefineNode ) } @@ -216,7 +240,7 @@ module.exports = { verifyEmit( emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], - nameLiteralNode, + nameWithLoc, vueDefineNode ) } @@ -227,11 +251,11 @@ module.exports = { return utils.defineTemplateBodyVisitor( context, { - /** @param { CallExpression & { argument: [Literal, ...Expression] } } node */ - 'CallExpression[arguments.0.type=Literal]'(node) { + /** @param { CallExpression } node */ + CallExpression(node) { const callee = utils.skipChainExpression(node.callee) - const nameLiteralNode = /** @type {Literal} */ (node.arguments[0]) - if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') { + const nameWithLoc = getNameParamNode(node) + if (!nameWithLoc) { // cannot check return } @@ -242,7 +266,7 @@ module.exports = { verifyEmit( vueTemplateDefineData.emits, vueTemplateDefineData.props, - nameLiteralNode, + nameWithLoc, vueTemplateDefineData.define ) } @@ -409,11 +433,11 @@ module.exports = { /** * @param {ObjectExpression|Program} define * @param {ComponentEmit[]} emits - * @param {Literal} nameNode + * @param {NameWithLoc} nameWithLoc * @param {RuleContext} context * @returns {Rule.SuggestionReportDescriptor[]} */ -function buildSuggest(define, emits, nameNode, context) { +function buildSuggest(define, emits, nameWithLoc, context) { const emitsKind = define.type === 'ObjectExpression' ? '`emits` option' : '`defineEmits`' const certainEmits = emits.filter((e) => e.key) @@ -423,18 +447,18 @@ function buildSuggest(define, emits, nameNode, context) { { messageId: 'addOneOption', data: { - name: `${nameNode.value}`, + name: nameWithLoc.name, emitsKind }, fix(fixer) { if (last.type === 'array') { // Array - return fixer.insertTextAfter(last.node, `, '${nameNode.value}'`) + return fixer.insertTextAfter(last.node, `, '${nameWithLoc.name}'`) } else if (last.type === 'object') { // Object return fixer.insertTextAfter( last.node, - `, '${nameNode.value}': null` + `, '${nameWithLoc.name}': null` ) } else { // type @@ -468,11 +492,11 @@ function buildSuggest(define, emits, nameNode, context) { return [ { messageId: 'addOneOption', - data: { name: `${nameNode.value}`, emitsKind }, + data: { name: `${nameWithLoc.name}`, emitsKind }, fix(fixer) { return fixer.insertTextAfter( leftBracket, - `'${nameNode.value}'${ + `'${nameWithLoc.name}'${ emitsOptionValue.elements.length > 0 ? ',' : '' }` ) @@ -486,11 +510,11 @@ function buildSuggest(define, emits, nameNode, context) { return [ { messageId: 'addOneOption', - data: { name: `${nameNode.value}`, emitsKind }, + data: { name: `${nameWithLoc.name}`, emitsKind }, fix(fixer) { return fixer.insertTextAfter( leftBrace, - `'${nameNode.value}': null${ + `'${nameWithLoc.name}': null${ emitsOptionValue.properties.length > 0 ? ',' : '' }` ) @@ -508,12 +532,12 @@ function buildSuggest(define, emits, nameNode, context) { return [ { messageId: 'addArrayEmitsOption', - data: { name: `${nameNode.value}`, emitsKind }, + data: { name: `${nameWithLoc.name}`, emitsKind }, fix(fixer) { if (afterOptionNode) { return fixer.insertTextAfter( sourceCode.getTokenBefore(afterOptionNode), - `\nemits: ['${nameNode.value}'],` + `\nemits: ['${nameWithLoc.name}'],` ) } else if (object.properties.length > 0) { const before = @@ -521,7 +545,7 @@ function buildSuggest(define, emits, nameNode, context) { object.properties[object.properties.length - 1] return fixer.insertTextAfter( before, - `,\nemits: ['${nameNode.value}']` + `,\nemits: ['${nameWithLoc.name}']` ) } else { const objectLeftBrace = /** @type {Token} */ ( @@ -532,7 +556,7 @@ function buildSuggest(define, emits, nameNode, context) { ) return fixer.insertTextAfter( objectLeftBrace, - `\nemits: ['${nameNode.value}']${ + `\nemits: ['${nameWithLoc.name}']${ objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? '' : '\n' @@ -543,12 +567,12 @@ function buildSuggest(define, emits, nameNode, context) { }, { messageId: 'addObjectEmitsOption', - data: { name: `${nameNode.value}`, emitsKind }, + data: { name: `${nameWithLoc.name}`, emitsKind }, fix(fixer) { if (afterOptionNode) { return fixer.insertTextAfter( sourceCode.getTokenBefore(afterOptionNode), - `\nemits: {'${nameNode.value}': null},` + `\nemits: {'${nameWithLoc.name}': null},` ) } else if (object.properties.length > 0) { const before = @@ -556,7 +580,7 @@ function buildSuggest(define, emits, nameNode, context) { object.properties[object.properties.length - 1] return fixer.insertTextAfter( before, - `,\nemits: {'${nameNode.value}': null}` + `,\nemits: {'${nameWithLoc.name}': null}` ) } else { const objectLeftBrace = /** @type {Token} */ ( @@ -567,7 +591,7 @@ function buildSuggest(define, emits, nameNode, context) { ) return fixer.insertTextAfter( objectLeftBrace, - `\nemits: {'${nameNode.value}': null}${ + `\nemits: {'${nameWithLoc.name}': null}${ objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? '' : '\n' diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js index 8ad20377e..f8e547992 100644 --- a/tests/lib/rules/custom-event-name-casing.js +++ b/tests/lib/rules/custom-event-name-casing.js @@ -621,6 +621,21 @@ tester.run('custom-event-name-casing', rule, { line: 5 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "Custom event name 'foo-bar' must be camelCase.", + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/no-restricted-custom-event.js b/tests/lib/rules/no-restricted-custom-event.js index 447f8122c..2dc4af4d6 100644 --- a/tests/lib/rules/no-restricted-custom-event.js +++ b/tests/lib/rules/no-restricted-custom-event.js @@ -291,6 +291,25 @@ tester.run('no-restricted-custom-event', rule, { line: 5 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['foo'], + errors: [ + { + message: 'Using `foo` event is not allowed.', + line: 5 + } + ] } ] }) diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index 1146cd2cc..9a22c74a9 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -1857,6 +1857,35 @@ emits: {'foo': null} line: 5 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + 'The "bar" event has been triggered but not declared on `defineEmits`.', + line: 5, + suggestions: [ + { + desc: 'Add the "bar" to `defineEmits`.', + output: ` + + ` + } + ] + } + ] } ] })