Skip to content

Commit 76077b4

Browse files
committed
⭐ new(rule): support no-raw-text rule
closes #2
1 parent 61e695c commit 76077b4

File tree

6 files changed

+294
-1
lines changed

6 files changed

+294
-1
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
| Rule ID | Description | |
1717
|:--------|:------------|:---|
1818
| [vue-i18n/<wbr>no-dynamic-keys](./no-dynamic-keys.html) | disallow localization dynamic keys at localization methods | |
19+
| [vue-i18n/<wbr>no-raw-text](./no-raw-text.html) | disallow to string literal in template or JSX | |
1920
| [vue-i18n/<wbr>no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | |
2021

Diff for: docs/rules/no-raw-text.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default {
5959
```js
6060
export default {
6161
// ✓ GOOD
62-
render: h => (<p>this.$t('hello')</p>)
62+
render: h => (<p>{this.$t('hello')}</p>)
6363
// ...
6464
}
6565
```

Diff for: lib/rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
'no-dynamic-keys': require('./rules/no-dynamic-keys'),
66
'no-html-messages': require('./rules/no-html-messages'),
77
'no-missing-keys': require('./rules/no-missing-keys'),
8+
'no-raw-text': require('./rules/no-raw-text'),
89
'no-unused-keys': require('./rules/no-unused-keys'),
910
'no-v-html': require('./rules/no-v-html')
1011
}

Diff for: lib/rules/no-raw-text.js

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const { parse, AST } = require('vue-eslint-parser')
7+
const { defineTemplateBodyVisitor } = require('../utils/index')
8+
9+
const hasOnlyLineBreak = value => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''))
10+
11+
function checkRawText (context, value, loc) {
12+
if (typeof value !== 'string' || hasOnlyLineBreak(value)) { return }
13+
14+
context.report({
15+
loc,
16+
message: `raw text '${value}' is used`
17+
})
18+
}
19+
20+
function findVariable (variables, name) {
21+
return variables.find(variable => variable.name === name)
22+
}
23+
24+
function getComponentTemplateValueNode (context, node) {
25+
const templateNode = node.properties
26+
.find(p =>
27+
p.type === 'Property' &&
28+
p.key.type === 'Identifier' &&
29+
p.key.name === 'template'
30+
)
31+
32+
if (templateNode) {
33+
if (templateNode.value.type === 'Literal') {
34+
return templateNode.value
35+
} else if (templateNode.value.type === 'Identifier') {
36+
const templateVariable = findVariable(context.getScope().variables, templateNode.value.name)
37+
const varDeclNode = templateVariable.defs[0].node
38+
if (varDeclNode.init && varDeclNode.init.type === 'Literal') {
39+
return varDeclNode.init
40+
}
41+
}
42+
}
43+
44+
return null
45+
}
46+
47+
function getComponentTemplateNode (value) {
48+
return parse(`<template>${value}</template>`).templateBody
49+
}
50+
51+
function create (context) {
52+
return defineTemplateBodyVisitor(context, { // template block
53+
VText (node) {
54+
checkRawText(context, node.value, node.loc)
55+
}
56+
}, { // script block or scripts
57+
ObjectExpression (node) {
58+
const valueNode = getComponentTemplateValueNode(context, node)
59+
if (!valueNode) { return }
60+
61+
const templateNode = getComponentTemplateNode(valueNode.value)
62+
AST.traverseNodes(templateNode, {
63+
enterNode (node) {
64+
if (node.type === 'VText') {
65+
checkRawText(context, node.value, valueNode.loc)
66+
}
67+
},
68+
leaveNode () {}
69+
})
70+
},
71+
72+
JSXText (node) {
73+
checkRawText(context, node.value, node.loc)
74+
}
75+
})
76+
}
77+
78+
module.exports = {
79+
meta: {
80+
type: 'suggestion',
81+
docs: {
82+
description: 'disallow to string literal in template or JSX',
83+
category: 'Best Practices',
84+
recommended: false
85+
},
86+
fixable: null,
87+
schema: []
88+
},
89+
create
90+
}

Diff for: lib/utils/index.js

+77
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,82 @@ function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor)
2626
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
2727
}
2828

29+
function unwrapTypes (node) {
30+
return node.type === 'TSAsExpression' ? node.expression : node
31+
}
32+
33+
function isVueFile (path) {
34+
return path.endsWith('.vue') || path.endsWith('.jsx')
35+
}
36+
37+
function isVueComponentFile (node, path) {
38+
return isVueFile(path) &&
39+
node.type === 'ExportDefaultDeclaration' &&
40+
node.declaration.type === 'ObjectExpression'
41+
}
42+
43+
function isVueComponent (node) {
44+
if (node.type === 'CallExpression') {
45+
const callee = node.callee
46+
47+
if (callee.type === 'MemberExpression') {
48+
const calleeObject = unwrapTypes(callee.object)
49+
50+
const isFullVueComponent = calleeObject.type === 'Identifier' &&
51+
calleeObject.name === 'Vue' &&
52+
callee.property.type === 'Identifier' &&
53+
['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
54+
node.arguments.length >= 1 &&
55+
node.arguments.slice(-1)[0].type === 'ObjectExpression'
56+
57+
return isFullVueComponent
58+
}
59+
60+
if (callee.type === 'Identifier') {
61+
const isDestructedVueComponent = callee.name === 'component' &&
62+
node.arguments.length >= 1 &&
63+
node.arguments.slice(-1)[0].type === 'ObjectExpression'
64+
65+
return isDestructedVueComponent
66+
}
67+
}
68+
69+
return false
70+
}
71+
72+
function executeOnVueComponent (context, cb) {
73+
const filePath = context.getFilename()
74+
const sourceCode = context.getSourceCode()
75+
const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
76+
const foundNodes = []
77+
78+
const isDuplicateNode = node => {
79+
if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) { return true }
80+
foundNodes.push(node)
81+
return false
82+
}
83+
84+
return {
85+
'ObjectExpression:exit' (node) {
86+
console.log('ObjectExpression', filePath, node)
87+
if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) { return }
88+
cb(node)
89+
},
90+
'ExportDefaultDeclaration:exit' (node) {
91+
console.log('ExportDefaultDeclaration', filePath, node)
92+
// export default {} in .vue || .js(x)
93+
if (isDuplicateNode(node.declaration)) { return }
94+
cb(node.declaration)
95+
},
96+
'CallExpression:exit' (node) {
97+
console.log('CallExpression:exit')
98+
// Vue.component('xxx', {}) || component('xxx', {})
99+
if (!isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) { return }
100+
cb(node.arguments.slice(-1)[0])
101+
}
102+
}
103+
}
104+
29105
function findExistLocaleMessage (fullpath, localeMessages) {
30106
return localeMessages.find(message => message.fullpath === fullpath)
31107
}
@@ -109,6 +185,7 @@ function generateJsonAst (context, json, filename) {
109185
module.exports = {
110186
UNEXPECTED_ERROR_LOCATION,
111187
defineTemplateBodyVisitor,
188+
executeOnVueComponent,
112189
getLocaleMessages,
113190
findMissingsFromLocaleMessages,
114191
findExistLocaleMessage,

Diff for: tests/lib/rules/no-raw-text.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const RuleTester = require('eslint').RuleTester
7+
const rule = require('../../../lib/rules/no-raw-text')
8+
9+
const tester = new RuleTester({
10+
parser: require.resolve('vue-eslint-parser'),
11+
parserOptions: {
12+
ecmaVersion: 2015,
13+
sourceType: 'module',
14+
ecmaFeatures: {
15+
jsx: true
16+
}
17+
}
18+
})
19+
20+
tester.run('no-raw-text', rule, {
21+
valid: [{
22+
code: `<template>
23+
<div class="app">
24+
<p class="a1">{{ $t('hello') }}</p>
25+
<p class="inner">{{ $t('click') }}<a href="/foo">{{ $t('here') }}</a>{{ $t('terminal') }}</p>
26+
</div>
27+
</template>`
28+
}, {
29+
filename: 'test.vue',
30+
code: `
31+
export default {
32+
template: '<p>{{ $t('hello') }}</p>'
33+
}
34+
`
35+
}, {
36+
code: `
37+
export default {
38+
render: function (h) {
39+
return (<p>{this.$t('hello')}</p>)
40+
}
41+
}
42+
`
43+
}],
44+
45+
invalid: [{
46+
// simple template
47+
code: `<template><p>hello</p></template>`,
48+
errors: [{
49+
message: `raw text 'hello' is used`, line: 1
50+
}]
51+
}, {
52+
// included newline or tab or space in simple template
53+
code: `
54+
<template>
55+
<p>hello</p>
56+
</template>
57+
`,
58+
errors: [{
59+
message: `raw text 'hello' is used`, line: 3
60+
}]
61+
}, {
62+
// child elements in template
63+
code: `
64+
<template>
65+
<div class="app">
66+
<p class="a1">hello</p>
67+
<p class="inner">click<a href="/foo">here</a>!</p>
68+
</div>
69+
</template>
70+
`,
71+
errors: [{
72+
message: `raw text 'hello' is used`, line: 4
73+
}, {
74+
message: `raw text 'click' is used`, line: 5
75+
}, {
76+
message: `raw text 'here' is used`, line: 5
77+
}, {
78+
message: `raw text '!' is used`, line: 5
79+
}]
80+
}, {
81+
// directly specify string literal to `template` component option at export default object
82+
code: `
83+
export default {
84+
template: '<p>hello</p>'
85+
}
86+
`,
87+
errors: [{
88+
message: `raw text 'hello' is used`, line: 3
89+
}]
90+
}, {
91+
// directly specify string literal to `template` component option at variable
92+
code: `
93+
const Component = {
94+
template: '<p>hello</p>'
95+
}
96+
`,
97+
errors: [{
98+
message: `raw text 'hello' is used`, line: 3
99+
}]
100+
}, {
101+
// directly specify string literal to `template` variable
102+
code: `
103+
const template = '<p>hello</p>'
104+
const Component = {
105+
template
106+
}
107+
`,
108+
errors: [{
109+
message: `raw text 'hello' is used`, line: 2
110+
}]
111+
}, {
112+
// directly specify string literal to JSX with `render`
113+
code: `
114+
const Component = {
115+
render () {
116+
return (<p>hello</p>)
117+
}
118+
}
119+
`,
120+
errors: [{
121+
message: `raw text 'hello' is used`, line: 4
122+
}]
123+
}]
124+
})

0 commit comments

Comments
 (0)