Skip to content

Commit

Permalink
Add vue/no-restricted-static-attribute rule (#1192)
Browse files Browse the repository at this point in the history
* Add `vue/no-restricted-static-attribute` rule

* fix
  • Loading branch information
ota-meshi committed Jun 7, 2020
1 parent ba230cd commit 898740e
Show file tree
Hide file tree
Showing 5 changed files with 417 additions and 1 deletion.
3 changes: 2 additions & 1 deletion docs/rules/README.md
Expand Up @@ -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()` | |
Expand Down
97 changes: 97 additions & 0 deletions 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"]
}
```

<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', 'foo', 'bar']}">

```vue
<template>
<!-- ✘ BAD -->
<div foo="x" />
<div bar />
</template>
```

</eslint-code-block>

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" }`

<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', value: 'bar' }]}">

```vue
<template>
<!-- ✓ GOOD -->
<div foo="foo" />
<!-- ✘ BAD -->
<div foo="bar" />
</template>
```

</eslint-code-block>

### `{ "key": "foo", "element": "MyButton" }`

<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', element: 'MyButton' }]}">

```vue
<template>
<!-- ✓ GOOD -->
<CoolButton foo="x" />
<!-- ✘ BAD -->
<MyButton foo="x" />
</template>
```

</eslint-code-block>

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

0 comments on commit 898740e

Please sign in to comment.