diff --git a/docs/rules/README.md b/docs/rules/README.md index fe113336e..d6413bba2 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -301,6 +301,7 @@ For example: | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | | +| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | | | [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | | | [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | diff --git a/docs/rules/no-restricted-props.md b/docs/rules/no-restricted-props.md new file mode 100644 index 000000000..664b6f2cd --- /dev/null +++ b/docs/rules/no-restricted-props.md @@ -0,0 +1,104 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-restricted-props +description: disallow specific props +--- +# vue/no-restricted-props +> disallow specific props + +## :book: Rule Details + +This rule allows you to specify props that you don't want to use in your application. + +## :wrench: Options + +This rule takes a list of strings, where each string is a prop name or pattern to be restricted: + +```json +{ + "vue/no-restricted-props": ["error", "value", "/^forbidden/"] +} +``` + + + +```vue + +``` + + + + + +```vue + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-props": ["error", + { + "name": "value", + "message": "If you intend a prop for v-model, it should be 'modelValue' in Vue 3.", + "suggest": "modelValue" + }, + ] +} +``` + +The following properties can be specified for the object. + +- `name` ... Specify the prop name or pattern. +- `message` ... Specify an optional custom message. +- `suggest` ... Specify an optional name to suggest changes. + + + +```vue + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-props.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-props.js) diff --git a/lib/index.js b/lib/index.js index 7755e9f1e..abaac1087 100644 --- a/lib/index.js +++ b/lib/index.js @@ -91,6 +91,7 @@ module.exports = { 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-component-options': require('./rules/no-restricted-component-options'), + 'no-restricted-props': require('./rules/no-restricted-props'), 'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), 'no-restricted-v-bind': require('./rules/no-restricted-v-bind'), diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js new file mode 100644 index 000000000..d1ba3bc49 --- /dev/null +++ b/lib/rules/no-restricted-props.js @@ -0,0 +1,149 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const regexp = require('../utils/regexp') + +/** + * @typedef {object} ParsedOption + * @property { (name: string) => boolean } test + * @property {string} [message] + * @property {string} [suggest] + */ + +/** + * @param {string} str + * @returns {(str: string) => boolean} + */ +function buildMatcher(str) { + if (regexp.isRegExp(str)) { + const re = regexp.toRegExp(str) + return (s) => { + re.lastIndex = 0 + return re.test(s) + } + } + return (s) => s === str +} +/** + * @param {string|{name:string, message?: string, suggest?:string}} option + * @returns {ParsedOption} + */ +function parseOption(option) { + if (typeof option === 'string') { + const matcher = buildMatcher(option) + return { + test(name) { + return matcher(name) + } + } + } + const parsed = parseOption(option.name) + parsed.message = option.message + parsed.suggest = option.suggest + return parsed +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific props', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-restricted-props.html' + }, + fixable: null, + schema: { + type: 'array', + items: { + oneOf: [ + { type: ['string'] }, + { + type: 'object', + properties: { + name: { type: 'string' }, + message: { type: 'string', minLength: 1 }, + suggest: { type: 'string' } + }, + required: ['name'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + }, + + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedProp: '{{message}}', + instead: 'Instead, change to `{{suggest}}`.' + } + }, + /** @param {RuleContext} context */ + create(context) { + /** @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 + } + + 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 + } + } + } + } + }) + } +} + +/** + * @param {Expression} node + * @param {ParsedOption} option + * @returns {Rule.SuggestionReportDescriptor[]} + */ +function createSuggest(node, option) { + if (!option.suggest) { + return [] + } + + /** @type {string} */ + let replaceText + if (node.type === 'Literal' || node.type === 'TemplateLiteral') { + replaceText = JSON.stringify(option.suggest) + } else if (node.type === 'Identifier') { + replaceText = /^[a-z]\w*$/iu.exec(option.suggest) + ? option.suggest + : JSON.stringify(option.suggest) + } else { + return [] + } + + return [ + { + fix(fixer) { + return fixer.replaceText(node, replaceText) + }, + messageId: 'instead', + data: { suggest: option.suggest } + } + ] +} diff --git a/tests/lib/rules/no-restricted-props.js b/tests/lib/rules/no-restricted-props.js new file mode 100644 index 000000000..4206b4014 --- /dev/null +++ b/tests/lib/rules/no-restricted-props.js @@ -0,0 +1,331 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-restricted-props') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } +}) + +tester.run('no-restricted-props', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['foo', 'bar', 'baz'] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['foo', 'bar', 'baz'] + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + options: ['bad'], + errors: [ + { + message: 'Using `bad` props is not allowed.', + line: 6, + column: 11 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['bad'], + errors: [ + { + message: 'Using `bad` props is not allowed.', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['/a/'], + errors: ['Using `bad` props is not allowed.'] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'bar' }, { name: 'baz', message: 'Using Baz' }], + errors: [ + { + message: 'Using `bar` props is not allowed.', + line: 6 + }, + { + message: 'Using Baz', + line: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { name: 'foo', suggest: 'Foo' }, + { name: 'bar', suggest: 'Bar' }, + { name: '0', suggest: 'Zero' } + ], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 5, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + }, + { + message: 'Using `bar` props is not allowed.', + line: 6, + suggestions: [ + { + desc: 'Instead, change to `Bar`.', + output: ` + + ` + } + ] + }, + { + message: 'Using `0` props is not allowed.', + line: 7, + suggestions: [ + { + desc: 'Instead, change to `Zero`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { name: 'foo', suggest: 'Foo' }, + { name: 'bar', suggest: 'b-a-r' }, + { name: 'baz', suggest: 'Baz' } + ], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 5, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + }, + { + message: 'Using `bar` props is not allowed.', + line: 6, + suggestions: [ + { + desc: 'Instead, change to `b-a-r`.', + output: ` + + ` + } + ] + }, + { + message: 'Using `baz` props is not allowed.', + line: 7, + suggestions: [ + { + desc: 'Instead, change to `Baz`.', + output: ` + + ` + } + ] + } + ] + } + ] +})