diff --git a/docs/rules/README.md b/docs/rules/README.md index 860b0f731..4239d3a21 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -287,14 +287,15 @@ For example: | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | | | [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-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 | | | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | | | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | | | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | | -| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | +| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | | diff --git a/docs/rules/no-restricted-static-attribute.md b/docs/rules/no-restricted-static-attribute.md new file mode 100644 index 000000000..a2009d7a1 --- /dev/null +++ b/docs/rules/no-restricted-static-attribute.md @@ -0,0 +1,97 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-restricted-static-attribute +description: disallow specific attribute +--- +# vue/no-restricted-static-attribute +> disallow specific attribute + +## :book: Rule Details + +This rule allows you to specify attribute names that you don't want to use in your application. + +## :wrench: Options + +This rule takes a list of strings, where each string is a attribute name or pattern to be restricted: + +```json +{ + "vue/no-restricted-static-attribute": ["error", "foo", "bar"] +} +``` + + + +```vue + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-static-attribute": ["error", + { + "key": "stlye", + "message": "Using \"stlye\" is not allowed. Use \"style\" instead." + } + ] +} +``` + +The following properties can be specified for the object. + +- `key` ... Specify the attribute key name or pattern. +- `value` ... Specify the value text or pattern or `true`. If specified, it will only be reported if the specified value is used. If `true`, it will only be reported if there is no value or if the value and key are same. +- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element. +- `message` ... Specify an optional custom message. + +### `{ "key": "foo", "value": "bar" }` + + + +```vue + +``` + + + +### `{ "key": "foo", "element": "MyButton" }` + + + +```vue + +``` + + + +## :couple: Related rules + +- [vue/no-restricted-v-bind] + +[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-static-attribute.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-static-attribute.js) diff --git a/lib/index.js b/lib/index.js index 974ea15e0..45d0d58fe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -80,6 +80,7 @@ module.exports = { 'no-ref-as-operand': require('./rules/no-ref-as-operand'), 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), + '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'), 'no-setup-props-destructure': require('./rules/no-setup-props-destructure'), diff --git a/lib/rules/no-restricted-static-attribute.js b/lib/rules/no-restricted-static-attribute.js new file mode 100644 index 000000000..70db824db --- /dev/null +++ b/lib/rules/no-restricted-static-attribute.js @@ -0,0 +1,166 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const regexp = require('../utils/regexp') + +/** + * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute + */ +/** + * @typedef {object} ParsedOption + * @property { (key: VAttribute) => boolean } test + * @property {boolean} [useValue] + * @property {boolean} [useElement] + * @property {string} [message] + */ + +/** + * @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 {any} option + * @returns {ParsedOption} + */ +function parseOption(option) { + if (typeof option === 'string') { + const matcher = buildMatcher(option) + return { + test({ key }) { + return matcher(key.rawName) + } + } + } + const parsed = parseOption(option.key) + if (option.value) { + const keyTest = parsed.test + if (option.value === true) { + parsed.test = (node) => { + if (!keyTest(node)) { + return false + } + return node.value == null || node.value.value === node.key.rawName + } + } else { + const valueMatcher = buildMatcher(option.value) + parsed.test = (node) => { + if (!keyTest(node)) { + return false + } + return node.value != null && valueMatcher(node.value.value) + } + } + parsed.useValue = true + } + if (option.element) { + const argTest = parsed.test + const tagMatcher = buildMatcher(option.element) + parsed.test = (node) => { + if (!argTest(node)) { + return false + } + const element = node.parent.parent + return tagMatcher(element.rawName) + } + parsed.useElement = true + } + parsed.message = option.message + return parsed +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific attribute', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-restricted-static-attribute.html' + }, + fixable: null, + schema: { + type: 'array', + items: { + oneOf: [ + { type: 'string' }, + { + type: 'object', + properties: { + key: { type: 'string' }, + value: { anyOf: [{ type: 'string' }, { enum: [true] }] }, + element: { type: 'string' }, + message: { type: 'string', minLength: 1 } + }, + required: ['key'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + }, + + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedAttr: '{{message}}' + } + }, + create(context) { + if (!context.options.length) { + return {} + } + /** @type {ParsedOption[]} */ + const options = context.options.map(parseOption) + + return utils.defineTemplateBodyVisitor(context, { + /** + * @param {VAttribute} node + */ + 'VAttribute[directive=false]'(node) { + for (const option of options) { + if (option.test(node)) { + const message = option.message || defaultMessage(node, option) + context.report({ + node, + messageId: 'restrictedAttr', + data: { message } + }) + return + } + } + } + }) + + /** + * @param {VAttribute} node + * @param {ParsedOption} option + */ + function defaultMessage(node, option) { + const key = node.key.rawName + const value = !option.useValue + ? '' + : node.value == null + ? '` set to `true' + : `="${node.value.value}"` + + let on = '' + if (option.useElement) { + on = ` on \`<${node.parent.parent.rawName}>\`` + } + return `Using \`${key + value}\`${on} is not allowed.` + } + } +} diff --git a/tests/lib/rules/no-restricted-static-attribute.js b/tests/lib/rules/no-restricted-static-attribute.js new file mode 100644 index 000000000..1b57e9145 --- /dev/null +++ b/tests/lib/rules/no-restricted-static-attribute.js @@ -0,0 +1,151 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-restricted-static-attribute') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2020 } +}) + +tester.run('no-restricted-static-attribute', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: [{ key: 'foo', value: 'bar' }] + }, + { + filename: 'test.vue', + code: '', + options: [{ key: 'foo', element: 'input' }] + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + options: ['foo'], + errors: [ + { + message: 'Using `foo` is not allowed.', + line: 1, + column: 16 + } + ] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'], + errors: ['Using `foo` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: ['/^f/'], + errors: ['Using `foo` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo', 'bar'], + errors: ['Using `foo` is not allowed.', 'Using `bar` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: [{ key: '/^(foo|bar)$/' }], + errors: ['Using `foo` is not allowed.', 'Using `bar` is not allowed.'] + }, + { + filename: 'test.vue', + code: '', + options: [{ key: 'foo', value: 'bar' }], + errors: ['Using `foo="bar"` is not allowed.'] + }, + { + filename: 'test.vue', + code: + '', + options: [ + '/^vv/', + { key: 'foo', value: true }, + { key: 'bar', value: '/^vv/' } + ], + errors: [ + 'Using `foo` set to `true` is not allowed.', + 'Using `foo="foo"` is not allowed.', + 'Using `vv` is not allowed.', + 'Using `vvv` is not allowed.', + 'Using `bar="vv"` is not allowed.' + ] + }, + { + filename: 'test.vue', + code: ` + `, + options: [{ key: 'foo', element: `/^My/` }], + errors: ['Using `foo` on `` is not allowed.'] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['/^f/', { key: 'foo' }], + errors: ['Using `foo` is not allowed.'] + }, + { + filename: 'test.vue', + code: ` + `, + options: [{ key: 'foo', message: 'foo' }], + errors: ['foo'] + } + ] +})