Skip to content

Commit e5c835e

Browse files
authored
Add vue/no-bare-strings-in-template rule (#1185)
1 parent af90fed commit e5c835e

File tree

7 files changed

+673
-2
lines changed

7 files changed

+673
-2
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ For example:
282282
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
283283
| [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
284284
| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | |
285+
| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` | |
285286
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
286287
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
287288
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-bare-strings-in-template
5+
description: disallow the use of bare strings in `<template>`
6+
---
7+
# vue/no-bare-strings-in-template
8+
> disallow the use of bare strings in `<template>`
9+
10+
## :book: Rule Details
11+
12+
This rule disallows the use of bare strings in `<template>`.
13+
In order to be able to internationalize your application, you will need to avoid using plain strings in your templates. Instead, you would need to use a template helper specializing in translation.
14+
15+
This rule was inspired by [no-bare-strings rule in ember-template-lint](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-bare-strings.md).
16+
17+
18+
<eslint-code-block :rules="{'vue/no-bare-strings-in-template': ['error']}">
19+
20+
```vue
21+
<template>
22+
<!-- ✓ GOOD -->
23+
<h1>{{ $t('foo.bar') }}</h1>
24+
<h1>{{ foo }}</h1>
25+
<h1 v-t="'foo.bar'"></h1>
26+
27+
<!-- ✗ BAD -->
28+
<h1>Lorem ipsum</h1>
29+
<div
30+
title="Lorem ipsum"
31+
aria-label="Lorem ipsum"
32+
aria-placeholder="Lorem ipsum"
33+
aria-roledescription="Lorem ipsum"
34+
aria-valuetext="Lorem ipsum"
35+
/>
36+
<img alt="Lorem ipsum">
37+
<input placeholder="Lorem ipsum">
38+
<h1 v-text="'Lorem ipsum'" />
39+
40+
<!-- Does not check -->
41+
<h1>{{ 'Lorem ipsum' }}</h1>
42+
<div
43+
v-bind:title="'Lorem ipsum'"
44+
/>
45+
</template>
46+
```
47+
48+
</eslint-code-block>
49+
50+
:::tip
51+
This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision.
52+
If you want to report these string literals, enable the [vue/no-useless-v-bind] and [vue/no-useless-mustaches] rules and fix the useless string literals.
53+
:::
54+
55+
## :wrench: Options
56+
57+
```js
58+
{
59+
"vue/no-bare-strings-in-template": ["error", {
60+
"whitelist": [
61+
"(", ")", ",", ".", "&", "+", "-", "=", "*", "/", "#", "%", "!", "?", ":", "[", "]", "{", "}", "<", ">", "\u00b7", "\u2022", "\u2010", "\u2013", "\u2014", "\u2212", "|"
62+
],
63+
"attributes": {
64+
"/.+/": ["title", "aria-label", "aria-placeholder", "aria-roledescription", "aria-valuetext"],
65+
"input": ["placeholder"],
66+
"img": ["alt"]
67+
},
68+
"directives": ["v-text"]
69+
}]
70+
}
71+
```
72+
73+
- `whitelist` ... An array of whitelisted strings.
74+
- `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name.
75+
- `directives` ... An array of directive names to check literal value.
76+
77+
## :couple: Related rules
78+
79+
- [vue/no-useless-v-bind]
80+
- [vue/no-useless-mustaches]
81+
82+
[vue/no-useless-v-bind]: ./no-useless-v-bind.md
83+
[vue/no-useless-mustaches]: ./no-useless-mustaches.md
84+
85+
## :mag: Implementation
86+
87+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-bare-strings-in-template.js)
88+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-bare-strings-in-template.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ module.exports = {
4545
'name-property-casing': require('./rules/name-property-casing'),
4646
'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
4747
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
48+
'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),
4849
'no-boolean-default': require('./rules/no-boolean-default'),
4950
'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
5051
'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const regexp = require('../utils/regexp')
13+
const casing = require('../utils/casing')
14+
15+
/**
16+
* @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute
17+
* @typedef {import('vue-eslint-parser').AST.VDirective} VDirective
18+
* @typedef {import('vue-eslint-parser').AST.VElement} VElement
19+
* @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier
20+
* @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
21+
* @typedef {import('vue-eslint-parser').AST.VText} VText
22+
*/
23+
24+
/**
25+
* @typedef { { names: { [tagName in string]: Set<string> }, regexps: { name: RegExp, attrs: Set<string> }[], cache: { [tagName in string]: Set<string> } } } TargetAttrs
26+
* @typedef { { upper: ElementStack, name: string, attrs: Set<string> } } ElementStack
27+
*/
28+
29+
// ------------------------------------------------------------------------------
30+
// Constants
31+
// ------------------------------------------------------------------------------
32+
33+
// https://dev.w3.org/html5/html-author/charref
34+
const DEFAULT_WHITELIST = [
35+
'(',
36+
')',
37+
',',
38+
'.',
39+
'&',
40+
'+',
41+
'-',
42+
'=',
43+
'*',
44+
'/',
45+
'#',
46+
'%',
47+
'!',
48+
'?',
49+
':',
50+
'[',
51+
']',
52+
'{',
53+
'}',
54+
'<',
55+
'>',
56+
'\u00b7', // "·"
57+
'\u2022', // "•"
58+
'\u2010', // "‐"
59+
'\u2013', // "–"
60+
'\u2014', // "—"
61+
'\u2212', // "−"
62+
'|'
63+
]
64+
65+
const DEFAULT_ATTRIBUTES = {
66+
'/.+/': [
67+
'title',
68+
'aria-label',
69+
'aria-placeholder',
70+
'aria-roledescription',
71+
'aria-valuetext'
72+
],
73+
input: ['placeholder'],
74+
img: ['alt']
75+
}
76+
77+
const DEFAULT_DIRECTIVES = ['v-text']
78+
79+
// --------------------------------------------------------------------------
80+
// Helpers
81+
// --------------------------------------------------------------------------
82+
83+
/**
84+
* Parse attributes option
85+
* @returns {TargetAttrs}
86+
*/
87+
function parseTargetAttrs(options) {
88+
/** @type {TargetAttrs} */
89+
const result = { names: {}, regexps: [], cache: {} }
90+
for (const tagName of Object.keys(options)) {
91+
/** @type { Set<string> } */
92+
const attrs = new Set(options[tagName])
93+
if (regexp.isRegExp(tagName)) {
94+
result.regexps.push({
95+
name: regexp.toRegExp(tagName),
96+
attrs
97+
})
98+
} else {
99+
result.names[tagName] = attrs
100+
}
101+
}
102+
return result
103+
}
104+
105+
/**
106+
* Get a string from given expression container node
107+
* @param {VExpressionContainer} node
108+
* @returns { string | null }
109+
*/
110+
function getStringValue(value) {
111+
const expression = value.expression
112+
if (!expression) {
113+
return null
114+
}
115+
if (expression.type !== 'Literal') {
116+
return null
117+
}
118+
if (typeof expression.value === 'string') {
119+
return expression.value
120+
}
121+
return null
122+
}
123+
124+
// ------------------------------------------------------------------------------
125+
// Rule Definition
126+
// ------------------------------------------------------------------------------
127+
128+
module.exports = {
129+
meta: {
130+
type: 'suggestion',
131+
docs: {
132+
description: 'disallow the use of bare strings in `<template>`',
133+
categories: undefined,
134+
url: 'https://eslint.vuejs.org/rules/no-bare-strings-in-template.html'
135+
},
136+
schema: [
137+
{
138+
type: 'object',
139+
properties: {
140+
whitelist: {
141+
type: 'array',
142+
items: { type: 'string' },
143+
uniqueItems: true
144+
},
145+
attributes: {
146+
type: 'object',
147+
patternProperties: {
148+
'^(?:\\S+|/.*/[a-z]*)$': {
149+
type: 'array',
150+
items: { type: 'string' },
151+
uniqueItems: true
152+
}
153+
},
154+
additionalProperties: false
155+
},
156+
directives: {
157+
type: 'array',
158+
items: { type: 'string', pattern: '^v-' },
159+
uniqueItems: true
160+
}
161+
}
162+
}
163+
],
164+
messages: {
165+
unexpected: 'Unexpected non-translated string used.',
166+
unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.'
167+
}
168+
},
169+
create(context) {
170+
const opts = context.options[0] || {}
171+
const whitelist = opts.whitelist || DEFAULT_WHITELIST
172+
const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
173+
const directives = opts.directives || DEFAULT_DIRECTIVES
174+
175+
const whitelistRe = new RegExp(
176+
whitelist.map((w) => regexp.escape(w)).join('|'),
177+
'gu'
178+
)
179+
180+
/** @type {ElementStack | null} */
181+
let elementStack = null
182+
/**
183+
* Gets the bare string from given string
184+
* @param {string} str
185+
*/
186+
function getBareString(str) {
187+
return str.trim().replace(whitelistRe, '').trim()
188+
}
189+
190+
/**
191+
* Get the attribute to be verified from the element name.
192+
* @param {string} tagName
193+
* @returns {Set<string>}
194+
*/
195+
function getTargetAttrs(tagName) {
196+
if (attributes.cache[tagName]) {
197+
return attributes.cache[tagName]
198+
}
199+
/** @type {string[]} */
200+
const result = []
201+
if (attributes.names[tagName]) {
202+
result.push(...attributes.names[tagName])
203+
}
204+
for (const { name, attrs } of attributes.regexps) {
205+
name.lastIndex = 0
206+
if (name.test(tagName)) {
207+
result.push(...attrs)
208+
}
209+
}
210+
if (casing.isKebabCase(tagName)) {
211+
result.push(...getTargetAttrs(casing.pascalCase(tagName)))
212+
}
213+
214+
return (attributes.cache[tagName] = new Set(result))
215+
}
216+
217+
return utils.defineTemplateBodyVisitor(context, {
218+
/** @param {VText} node */
219+
VText(node) {
220+
if (getBareString(node.value)) {
221+
context.report({
222+
node,
223+
messageId: 'unexpected'
224+
})
225+
}
226+
},
227+
/**
228+
* @param {VElement} node
229+
*/
230+
VElement(node) {
231+
elementStack = {
232+
upper: elementStack,
233+
name: node.rawName,
234+
attrs: getTargetAttrs(node.rawName)
235+
}
236+
},
237+
'VElement:exit'() {
238+
elementStack = elementStack.upper
239+
},
240+
/** @param {VAttribute|VDirective} node */
241+
VAttribute(node) {
242+
if (!node.value) {
243+
return
244+
}
245+
if (node.directive === false) {
246+
const attrs = elementStack.attrs
247+
if (!attrs.has(node.key.rawName)) {
248+
return
249+
}
250+
251+
if (getBareString(node.value.value)) {
252+
context.report({
253+
node: node.value,
254+
messageId: 'unexpectedInAttr',
255+
data: {
256+
attr: node.key.rawName
257+
}
258+
})
259+
}
260+
} else {
261+
const directive = `v-${node.key.name.name}`
262+
if (!directives.includes(directive)) {
263+
return
264+
}
265+
const str = getStringValue(node.value)
266+
if (str && getBareString(str)) {
267+
context.report({
268+
node: node.value,
269+
messageId: 'unexpectedInAttr',
270+
data: {
271+
attr: directive
272+
}
273+
})
274+
}
275+
}
276+
}
277+
})
278+
}
279+
}

0 commit comments

Comments
 (0)