diff --git a/docs/rules/README.md b/docs/rules/README.md index 16e0ef983..fa734f4ca 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -288,6 +288,7 @@ 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-component-options](./no-restricted-component-options.md) | disallow specific component option | | | [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-component-options.md b/docs/rules/no-restricted-component-options.md new file mode 100644 index 000000000..41292bc67 --- /dev/null +++ b/docs/rules/no-restricted-component-options.md @@ -0,0 +1,124 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-restricted-component-options +description: disallow specific component option +--- +# vue/no-restricted-component-options +> disallow specific component option + +## :book: Rule Details + +This rule allows you to specify component options that you don't want to use in your application. + +## :wrench: Options + +This rule takes a list of strings, where each string is a component option name or pattern to be restricted: + +```json +{ + "vue/no-restricted-component-options": ["error", "init", "beforeCompile", "compiled", "activate", "ready", "/^(?:at|de)tached$/"] +} +``` + + + +```vue + +``` + + + +Also, you can use an array to specify the path of object properties. + +e.g. `[ "error", ["props", "/.*/", "twoWay"] ]` + + + +```vue + +``` + + + +You can use `"*"` to match all properties, including computed keys. + +e.g. `[ "error", ["props", "*", "twoWay"] ]` + + + +```vue + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-component-options": ["error", + { + "name": "init", + "message": "Use \"beforeCreate\" instead." + }, + { + "name": "/^(?:at|de)tached$/", + "message": "\"attached\" and \"detached\" is deprecated." + }, + { + "name": ["props", "/.*/", "twoWay"], + "message": "\"props.*.twoWay\" cannot be used." + } + ] +} +``` + +The following properties can be specified for the object. + +- `name` ... Specify the component option name or pattern, or the path by its array. +- `message` ... Specify an optional custom message. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-component-options.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-component-options.js) diff --git a/lib/index.js b/lib/index.js index 7eda5423d..81f7dfc72 100644 --- a/lib/index.js +++ b/lib/index.js @@ -82,6 +82,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-component-options': require('./rules/no-restricted-component-options'), '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-component-options.js b/lib/rules/no-restricted-component-options.js new file mode 100644 index 000000000..6d0de9a8d --- /dev/null +++ b/lib/rules/no-restricted-component-options.js @@ -0,0 +1,215 @@ +/** + * @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 {Tester} test + * @property {string|undefined} [message] + */ +/** + * @typedef {object} MatchResult + * @property {Tester | undefined} [next] + * @property {boolean} [wildcard] + * @property {string} keyName + */ +/** + * @typedef { (name: string) => boolean } Matcher + * @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester + */ + +/** + * @param {string} str + * @returns {Matcher} + */ +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 | string[] | { name: string | string[], message?: string } } option + * @returns {ParsedOption} + */ +function parseOption(option) { + if (typeof option === 'string' || Array.isArray(option)) { + return parseOption({ + name: option + }) + } + + /** + * @typedef {object} Step + * @property {Matcher} [test] + * @property {boolean} [wildcard] + */ + + /** @type {Step[]} */ + const steps = [] + for (const name of Array.isArray(option.name) ? option.name : [option.name]) { + if (name === '*') { + steps.push({ wildcard: true }) + } else { + steps.push({ test: buildMatcher(name) }) + } + } + const message = option.message + + return { + test: buildTester(0), + message + } + + /** + * @param {number} index + * @returns {Tester} + */ + function buildTester(index) { + const { wildcard, test } = steps[index] + const next = index + 1 + const needNext = steps.length > next + return (node) => { + /** @type {string} */ + let keyName + if (wildcard) { + keyName = '*' + } else { + if (node.type !== 'Property') { + return null + } + const name = utils.getStaticPropertyName(node) + if (!name || !test(name)) { + return null + } + keyName = name + } + + return { + next: needNext ? buildTester(next) : undefined, + wildcard, + keyName + } + } + } +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific component option', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-restricted-component-options.html' + }, + fixable: null, + schema: { + type: 'array', + items: { + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string' + } + }, + { + type: 'object', + properties: { + name: { + anyOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + message: { type: 'string', minLength: 1 } + }, + required: ['name'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + }, + + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedOption: '{{message}}' + } + }, + /** @param {RuleContext} context */ + create(context) { + if (!context.options || context.options.length === 0) { + return {} + } + /** @type {ParsedOption[]} */ + const options = context.options.map(parseOption) + + return utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + for (const option of options) { + verify(node, option.test, option.message) + } + } + }) + + /** + * @param {ObjectExpression} node + * @param {Tester} test + * @param {string | undefined} customMessage + * @param {string[]} path + */ + function verify(node, test, customMessage, path = []) { + for (const prop of node.properties) { + const result = test(prop) + if (!result) { + continue + } + if (result.next) { + if ( + prop.type !== 'Property' || + prop.value.type !== 'ObjectExpression' + ) { + continue + } + verify(prop.value, result.next, customMessage, [ + ...path, + result.keyName + ]) + } else { + const message = + customMessage || defaultMessage([...path, result.keyName]) + context.report({ + node: prop.type === 'Property' ? prop.key : prop, + messageId: 'restrictedOption', + data: { message } + }) + } + } + } + + /** + * @param {string[]} path + */ + function defaultMessage(path) { + return `Using \`${path.join('.')}\` is not allowed.` + } + } +} diff --git a/tests/lib/rules/no-restricted-component-options.js b/tests/lib/rules/no-restricted-component-options.js new file mode 100644 index 000000000..512eb53c5 --- /dev/null +++ b/tests/lib/rules/no-restricted-component-options.js @@ -0,0 +1,317 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-restricted-component-options') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } +}) + +tester.run('no-restricted-component-options', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + 'init', + 'beforeCompile', + 'compiled', + 'activate', + 'ready', + '/^(?:at|de)tached$/' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['foo', '*', 'baz']] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['foo', 'bar']] + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + options: [ + 'init', + 'beforeCompile', + 'compiled', + 'activate', + 'ready', + '/^(?:at|de)tached$/' + ], + errors: [ + { + message: 'Using `init` is not allowed.', + line: 5, + column: 9 + }, + { + message: 'Using `beforeCompile` is not allowed.', + line: 6, + column: 9 + }, + { + message: 'Using `compiled` is not allowed.', + line: 7, + column: 9 + }, + { + message: 'Using `activate` is not allowed.', + line: 8, + column: 9 + }, + { + message: 'Using `ready` is not allowed.', + line: 9, + column: 9 + }, + { + message: 'Using `attached` is not allowed.', + line: 10, + column: 9 + }, + { + message: 'Using `detached` is not allowed.', + line: 11, + column: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['props', '/.*/', 'twoWay']], + errors: [ + { + message: 'Using `props.name.twoWay` is not allowed.', + line: 10 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { + name: 'init', + message: 'Use "beforeCreate" instead.' + }, + { + name: '/^(?:at|de)tached$/', + message: '"attached" and "detached" is deprecated.' + }, + { + name: ['props', '/.*/', 'twoWay'], + message: '"props.*.twoWay" cannot be used.' + } + ], + errors: [ + { + message: '"props.*.twoWay" cannot be used.', + line: 10, + column: 13 + }, + { + message: 'Use "beforeCreate" instead.', + line: 13, + column: 9 + }, + { + message: '"attached" and "detached" is deprecated.', + line: 18, + column: 9 + }, + { + message: '"attached" and "detached" is deprecated.', + line: 19, + column: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['props', '*', 'twoWay']], + errors: [ + { + message: 'Using `props.*.twoWay` is not allowed.', + line: 10 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['foo', '*']], + errors: [ + { + message: 'Using `foo.*` is not allowed.', + line: 5 + } + ] + } + ] +})