Skip to content

Commit

Permalink
Add vue/no-restricted-component-options rule (#1213)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jun 26, 2020
1 parent 04f83ba commit dbba30d
Show file tree
Hide file tree
Showing 5 changed files with 658 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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 | |
Expand Down
124 changes: 124 additions & 0 deletions 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$/"]
}
```

<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error', 'init', 'beforeCompile', 'compiled', 'activate', 'ready', '/^(?:at|de)tached$/']}">

```vue
<script>
export default {
/* ✗ BAD */
init: function () {},
beforeCompile: function () {},
compiled: function () {},
activate: function () {},
ready: function () {},
attached: function () {},
detached: function () {},
/* ✓ GOOD */
beforeCreate: function () {},
activated: function () {},
mounted: function () {},
}
</script>
```

</eslint-code-block>

Also, you can use an array to specify the path of object properties.

e.g. `[ "error", ["props", "/.*/", "twoWay"] ]`

<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '/.*/', 'twoWay'] ]}">

```vue
<script>
export default {
props: {
size: Number,
name: {
type: String,
required: true,
/* ✗ BAD */
twoWay: true
}
}
}
</script>
```

</eslint-code-block>

You can use `"*"` to match all properties, including computed keys.

e.g. `[ "error", ["props", "*", "twoWay"] ]`

<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '*', 'twoWay'] ]}">

```vue
<script>
export default {
props: {
[foo + bar]: {
type: String,
required: true,
/* ✗ BAD */
twoWay: true
}
}
}
</script>
```

</eslint-code-block>

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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
215 changes: 215 additions & 0 deletions 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.`
}
}
}

0 comments on commit dbba30d

Please sign in to comment.