Skip to content

Commit ab85fd6

Browse files
doug-wadeFloEdelmannota-meshi
authored
Add vue/no-invalid-attribute-name rule (#1851)
* Fix #1373: Add rule no-invalid-attribute-name * Remove stray newline * Apply suggestions from code review Co-authored-by: Flo Edelmann <[email protected]> * #1373 Use xml-name-validator * Fix linting error * remove stray newline * refactor test code * Update lib/rules/no-invalid-attribute-name.js Co-authored-by: Flo Edelmann <[email protected]> * fix bad commit from github ui * fix typechecking error * Respond to PR feedback * Include the added types in package.json * check v-bind directives * Update tests/lib/rules/no-invalid-attribute-name.js Co-authored-by: Flo Edelmann <[email protected]> * Fix failing unit test * Update lib/rules/no-invalid-attribute-name.js * Update lib/rules/no-invalid-attribute-name.js * Update tests/lib/rules/no-invalid-attribute-name.js * Update tests/lib/rules/no-invalid-attribute-name.js Co-authored-by: Flo Edelmann <[email protected]> Co-authored-by: Yosuke Ota <[email protected]>
1 parent b0639d7 commit ab85fd6

File tree

6 files changed

+263
-1
lines changed

6 files changed

+263
-1
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ For example:
224224
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: | :hammer: |
225225
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | | :hammer: |
226226
| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | | :hammer: |
227+
| [vue/no-invalid-attribute-name](./no-invalid-attribute-name.md) | require valid attribute names | | :warning: |
227228
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | | :hammer: |
228229
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
229230
| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block | | :hammer: |
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-invalid-attribute-name
5+
description: require valid attribute names
6+
---
7+
# vue/no-invalid-attribute-name
8+
9+
> require valid attribute names
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule detects invalid HTML attributes.
16+
17+
<eslint-code-block :rules="{'vue/no-invalid-attribute-name': ['error']}">
18+
19+
```vue
20+
<template>
21+
<!-- ✓ GOOD -->
22+
<p foo.bar></p>
23+
<p foo-bar></p>
24+
<p _foo.bar></p>
25+
<p :foo-bar></p>
26+
27+
<!-- ✗ BAD -->
28+
<p 0abc></p>
29+
<p -def></p>
30+
<p !ghi></p>
31+
</template>
32+
```
33+
34+
</eslint-code-block>
35+
36+
## :wrench: Options
37+
38+
Nothing.
39+
40+
## :mag: Implementation
41+
42+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-invalid-attribute-name.js)
43+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-invalid-attribute-name.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ module.exports = {
9191
'no-export-in-script-setup': require('./rules/no-export-in-script-setup'),
9292
'no-expose-after-await': require('./rules/no-expose-after-await'),
9393
'no-extra-parens': require('./rules/no-extra-parens'),
94+
'no-invalid-attribute-name': require('./rules/no-invalid-attribute-name'),
9495
'no-invalid-model-keys': require('./rules/no-invalid-model-keys'),
9596
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
9697
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @author Doug Wade <[email protected]>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const xnv = require('xml-name-validator')
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'require valid attribute names',
15+
categories: undefined,
16+
url: 'https://eslint.vuejs.org/rules/no-invalid-attribute-name.html'
17+
},
18+
fixable: null,
19+
schema: [],
20+
messages: {
21+
attribute: 'Attribute name {{name}} is not valid.'
22+
}
23+
},
24+
/** @param {RuleContext} context */
25+
create(context) {
26+
/**
27+
* @param {string | VIdentifier} key
28+
* @return {string}
29+
*/
30+
const getName = (key) => (typeof key === 'string' ? key : key.name)
31+
32+
return utils.defineTemplateBodyVisitor(context, {
33+
/** @param {VDirective | VAttribute} node */
34+
VAttribute(node) {
35+
if (utils.isCustomComponent(node.parent.parent)) {
36+
return
37+
}
38+
39+
const name = getName(node.key.name)
40+
41+
if (
42+
node.directive &&
43+
name === 'bind' &&
44+
node.key.argument &&
45+
node.key.argument.type === 'VIdentifier' &&
46+
!xnv.name(node.key.argument.name)
47+
) {
48+
context.report({
49+
node,
50+
messageId: 'attribute',
51+
data: {
52+
name: node.key.argument.name
53+
}
54+
})
55+
}
56+
57+
if (!node.directive && !xnv.name(name)) {
58+
context.report({
59+
node,
60+
messageId: 'attribute',
61+
data: {
62+
name
63+
}
64+
})
65+
}
66+
}
67+
})
68+
}
69+
}

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,16 @@
5959
"nth-check": "^2.0.1",
6060
"postcss-selector-parser": "^6.0.9",
6161
"semver": "^7.3.5",
62-
"vue-eslint-parser": "^9.0.1"
62+
"vue-eslint-parser": "^9.0.1",
63+
"xml-name-validator": "^4.0.0"
6364
},
6465
"devDependencies": {
6566
"@types/eslint": "^8.4.2",
6667
"@types/eslint-visitor-keys": "^1.0.0",
6768
"@types/natural-compare": "^1.4.1",
6869
"@types/node": "^13.13.5",
6970
"@types/semver": "^7.3.9",
71+
"@types/xml-name-validator": "^4.0.0",
7072
"@typescript-eslint/parser": "^5.23.0",
7173
"@vuepress/plugin-pwa": "^1.9.7",
7274
"acorn": "^8.7.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @author *****your name*****
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../lib/rules/no-invalid-attribute-name')
9+
10+
const tester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: {
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('no-invalid-attribute-name', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: '<template><p foo /></template>'
23+
},
24+
{
25+
filename: 'test.vue',
26+
code: `<template><p foo="bar" /></template>`
27+
},
28+
{
29+
filename: 'test.vue',
30+
code: `<template><p foo-bar /></template>`
31+
},
32+
{
33+
filename: 'test.vue',
34+
code: `<template><p _foo-bar /></template>`
35+
},
36+
{
37+
filename: 'test.vue',
38+
code: `<template><p :foo-bar /></template>`
39+
},
40+
{
41+
filename: 'test.vue',
42+
code: `<template><p foo.bar /></template>`
43+
},
44+
{
45+
filename: 'test.vue',
46+
code: `<template><p quux-.9 /></template>`
47+
},
48+
{
49+
filename: 'test.vue',
50+
code: `<template><MyComponent 0abc="foo" /></template>`
51+
},
52+
{
53+
filename: 'test.vue',
54+
code: `<template><MyComponent :0abc="foo" /></template>`
55+
},
56+
{
57+
filename: 'test.vue',
58+
code: `<template><a :href="url"> ... </a></template>`
59+
},
60+
{
61+
filename: 'test.vue',
62+
code: `<template><div v-bind:class="{ active: isActive }"></div></template>`
63+
},
64+
{
65+
filename: 'test.vue',
66+
code: `<template><p v-if="seen">Now you see me</p></template>`
67+
},
68+
{
69+
filename: 'test.vue',
70+
code: `<a v-on:[eventName]="doSomething"> ... </a>`
71+
},
72+
{
73+
filename: 'test.vue',
74+
code: `<form v-on:submit.prevent="onSubmit"> ... </form>`
75+
},
76+
{
77+
filename: 'test.vue',
78+
code: `<a @[event]="doSomething"> ... </a>`
79+
},
80+
{
81+
filename: 'test.vue',
82+
code: `<template><div v-bind="..."></div></template>`
83+
},
84+
{
85+
filename: 'test.vue',
86+
code: `<template><div v-0abc="..."></div></template>`
87+
}
88+
],
89+
invalid: [
90+
{
91+
filename: 'test.vue',
92+
code: `<template><p 0abc /></template>`,
93+
errors: [
94+
{
95+
message: 'Attribute name 0abc is not valid.',
96+
line: 1,
97+
column: 14
98+
}
99+
]
100+
},
101+
{
102+
filename: 'test.vue',
103+
code: `<template><p -def></template>`,
104+
errors: [
105+
{
106+
message: 'Attribute name -def is not valid.',
107+
line: 1,
108+
column: 14
109+
}
110+
]
111+
},
112+
{
113+
filename: 'test.vue',
114+
code: `<template><p !ghi /></template>`,
115+
errors: [
116+
{
117+
message: 'Attribute name !ghi is not valid.',
118+
line: 1,
119+
column: 14
120+
}
121+
]
122+
},
123+
{
124+
filename: 'test.vue',
125+
code: `<template><p v-bind:0abc=""></template>`,
126+
errors: [
127+
{
128+
message: 'Attribute name 0abc is not valid.',
129+
line: 1,
130+
column: 14
131+
}
132+
]
133+
},
134+
{
135+
filename: 'test.vue',
136+
code: `<template><p :0abc="..." /></template>`,
137+
errors: [
138+
{
139+
message: 'Attribute name 0abc is not valid.',
140+
line: 1,
141+
column: 14
142+
}
143+
]
144+
}
145+
]
146+
})

0 commit comments

Comments
 (0)