diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js index 8a64913c3..9bd26a81e 100644 --- a/lib/rules/no-restricted-props.js +++ b/lib/rules/no-restricted-props.js @@ -7,6 +7,12 @@ const utils = require('../utils') const regexp = require('../utils/regexp') +/** + * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp + * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp + * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp + */ + /** * @typedef {object} ParsedOption * @property { (name: string) => boolean } test @@ -88,39 +94,58 @@ module.exports = { /** @type {ParsedOption[]} */ const options = context.options.map(parseOption) - return utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - for (const prop of utils.getComponentProps(node)) { - if (!prop.propName) { - continue - } + /** + * @param {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props + * @param { { [key: string]: Property | undefined } } [withDefaultsProps] + */ + function processProps(props, withDefaultsProps) { + for (const prop of props) { + if (!prop.propName) { + continue + } - for (const option of options) { - if (option.test(prop.propName)) { - const message = - option.message || - `Using \`${prop.propName}\` props is not allowed.` - context.report({ - node: prop.key, - messageId: 'restrictedProp', - data: { message }, - suggest: createSuggest(prop.key, option) - }) - break - } + for (const option of options) { + if (option.test(prop.propName)) { + const message = + option.message || + `Using \`${prop.propName}\` props is not allowed.` + context.report({ + node: prop.key, + messageId: 'restrictedProp', + data: { message }, + suggest: createSuggest( + prop.key, + option, + withDefaultsProps && withDefaultsProps[prop.propName] + ) + }) + break } } } - }) + } + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, props) { + processProps(props, utils.getWithDefaultsProps(node)) + } + }), + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + processProps(utils.getComponentProps(node)) + } + }) + ) } } /** * @param {Expression} node * @param {ParsedOption} option + * @param {Property} [withDefault] * @returns {Rule.SuggestionReportDescriptor[]} */ -function createSuggest(node, option) { +function createSuggest(node, option, withDefault) { if (!option.suggest) { return [] } @@ -140,7 +165,17 @@ function createSuggest(node, option) { return [ { fix(fixer) { - return fixer.replaceText(node, replaceText) + const fixes = [fixer.replaceText(node, replaceText)] + if (withDefault) { + if (withDefault.shorthand) { + fixes.push( + fixer.insertTextBefore(withDefault.value, `${replaceText}:`) + ) + } else { + fixes.push(fixer.replaceText(withDefault.key, replaceText)) + } + } + return fixes.sort((a, b) => a.range[0] - b.range[0]) }, messageId: 'instead', data: { suggest: option.suggest } diff --git a/lib/utils/index.js b/lib/utils/index.js index dc7f3d280..9b86cf6e4 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1152,29 +1152,24 @@ module.exports = { * @returns { { [key: string]: Expression | undefined } } */ getWithDefaultsPropExpressions(node) { - if (!hasWithDefaults(node)) { - return {} - } - const param = node.parent.arguments[1] - if (!param || param.type !== 'ObjectExpression') { - return {} - } + const map = getWithDefaultsProps(node) - /** @type {Record} */ + /** @type {Record} */ const result = {} - for (const prop of param.properties) { - if (prop.type !== 'Property') { - return {} - } - const name = getStaticPropertyName(prop) - if (name != null) { - result[name] = prop.value - } + for (const key of Object.keys(map)) { + const prop = map[key] + result[key] = prop && prop.value } return result }, + /** + * Gets a map of the property nodes defined in withDefaults. + * @param {CallExpression} node The node of defineProps + * @returns { { [key: string]: Property | undefined } } + */ + getWithDefaultsProps, getVueObjectType, /** @@ -2400,6 +2395,36 @@ function hasWithDefaults(node) { ) } +/** + * Gets a map of the property nodes defined in withDefaults. + * @param {CallExpression} node The node of defineProps + * @returns { { [key: string]: Property | undefined } } + */ +function getWithDefaultsProps(node) { + if (!hasWithDefaults(node)) { + return {} + } + const param = node.parent.arguments[1] + if (!param || param.type !== 'ObjectExpression') { + return {} + } + + /** @type {Record} */ + const result = {} + + for (const prop of param.properties) { + if (prop.type !== 'Property') { + return {} + } + const name = getStaticPropertyName(prop) + if (name != null) { + result[name] = prop + } + } + + return result +} + /** * Get all props by looking at all component's properties * @param {ObjectExpression|ArrayExpression} propsNode Object with props definition diff --git a/tests/lib/rules/no-restricted-props.js b/tests/lib/rules/no-restricted-props.js index 4206b4014..3348ca1a0 100644 --- a/tests/lib/rules/no-restricted-props.js +++ b/tests/lib/rules/no-restricted-props.js @@ -7,6 +7,7 @@ // Requirements // ------------------------------------------------------------------------------ +const semver = require('semver') const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/no-restricted-props') @@ -326,6 +327,265 @@ tester.run('no-restricted-props', rule, { ] } ] - } + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 4, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 3, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 4, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + ...(semver.lt( + require('@typescript-eslint/parser/package.json').version, + '4.0.0' + ) + ? [] + : [ + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 4, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 4, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 7, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + }, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 9, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + } + ]) ] })