Skip to content

Commit 276f1ad

Browse files
committed
feat: add attribute-name-validator rule
1 parent edb7ec5 commit 276f1ad

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/attribute-name-validator
5+
description: Disable some invalid attribute name of custom component by regex pattern, e.g. `/\\w+/-id`, `/id/`, not letter, or other abuseful names.
6+
---
7+
# vue/attribute-name-validator
8+
9+
> Disable some invalid attribute name of custom component by regex pattern, e.g. `/\\w+/-id`, `/id/`, not letter, or other abuseful 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 use regex pattern to test the attribute name of custom component.
16+
17+
Some useful pattern:
18+
19+
- allow all letters and numbers: `/^([a-zA-Z_$][a-zA-Z\d_$]*)$/`
20+
- disable attribute `ref`: `/[^(ref)]/`
21+
22+
<eslint-code-block :rules="{'vue/attribute-name-validator': [/id/]}">
23+
24+
```vue
25+
<template>
26+
<CustomComponent jd="957"/>
27+
</template>
28+
```
29+
30+
</eslint-code-block>

lib/rules/attribute-name-validator.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* @author youxingz
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description: 'Disable some invalid attribute name of custom component by regex pattern, e.g. `/\\w+/-id`, `/id/`, not letter, or other abuseful names.',
14+
categories: ['attribute-name'],
15+
url: 'https://eslint.vuejs.org/rules/attribute-name-validator.html'
16+
},
17+
fixable: 'code',
18+
schema: [
19+
{
20+
anyOf: [
21+
{ type: 'string' },
22+
{ type: 'object' },
23+
{
24+
type: 'array',
25+
items: {
26+
anyOf: [
27+
{ type: 'string' },
28+
{ type: 'object' }
29+
]
30+
},
31+
uniqueItems: true,
32+
additionalItems: false
33+
}
34+
]
35+
}
36+
],
37+
messages: {
38+
unexpected: 'Attribute name `{{name}}` is invalid.'
39+
}
40+
},
41+
/** @param {RuleContext} context */
42+
create(context) {
43+
// ...
44+
/**
45+
* @type {RegExp[]}
46+
*/
47+
const regexList = []
48+
if (typeof context.options === 'string') {
49+
regexList.push(new RegExp(context.options))
50+
} else if (context.options instanceof RegExp) {
51+
regexList.push(context.options)
52+
} else if (context.options.length > 0) {
53+
context.options.forEach(regex => {
54+
if (regex instanceof RegExp) {
55+
regexList.push(regex)
56+
} else {
57+
regexList.push(new RegExp(regex))
58+
}
59+
})
60+
}
61+
const getName = (/** @type {string | import("../../typings/eslint-plugin-vue/util-types/ast").VIdentifier} */ key) => (typeof key === 'string' ? key : key.name)
62+
const matchAtLeastOneRule = (/** @type {string} */ name) => {
63+
if (regexList.length === 0) return true
64+
for (let regex of regexList) {
65+
if (regex.test(name)) return true
66+
}
67+
return false
68+
}
69+
70+
return utils.defineTemplateBodyVisitor(context, {
71+
VAttribute(node) {
72+
if (!utils.isCustomComponent(node.parent.parent)) {
73+
// ignore custom which is not defined by user.
74+
return
75+
}
76+
77+
const name = getName(node.key.name)
78+
let didReport = false
79+
if (!matchAtLeastOneRule(name)) {
80+
didReport = true
81+
context.report({
82+
node,
83+
messageId: 'unexpected',
84+
data: { name }
85+
})
86+
}
87+
if (
88+
node.directive &&
89+
name === 'bind' &&
90+
node.key.argument &&
91+
node.key.argument.type === 'VIdentifier' &&
92+
!matchAtLeastOneRule(node.key.argument.name)
93+
) {
94+
!didReport &&
95+
context.report({
96+
node,
97+
messageId: 'unexpected',
98+
data: {
99+
name: node.key.argument.name
100+
}
101+
})
102+
didReport = true
103+
}
104+
105+
if (!node.directive && !matchAtLeastOneRule(name)) {
106+
!didReport &&
107+
context.report({
108+
node,
109+
messageId: 'unexpected',
110+
data: { name }
111+
})
112+
didReport = true
113+
}
114+
}
115+
})
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @author youxingz
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/attribute-name-validator')
9+
10+
const tester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: { ecmaVersion: 2015 }
13+
})
14+
15+
tester.run('attribute-name-validator', rule, {
16+
valid: [
17+
{
18+
filename: 'test.vue',
19+
code: `
20+
<template>
21+
<CustomComponent id="demo"/>
22+
</template>
23+
`,
24+
options: [/id/]
25+
},
26+
],
27+
invalid: [
28+
{
29+
filename: 'test.vue',
30+
code: `
31+
<template>
32+
<CustomComponent jd="demo"></CustomComponent>
33+
</template>
34+
`,
35+
options: [/id/],
36+
errors: [
37+
{
38+
message: 'Attribute name `jd` is invalid.',
39+
},
40+
]
41+
}
42+
]
43+
})

0 commit comments

Comments
 (0)