Skip to content

Commit 41b8294

Browse files
authored
feat: support template literal for no-raw-text rule (#229)
1 parent e031cc2 commit 41b8294

File tree

3 files changed

+255
-59
lines changed

3 files changed

+255
-59
lines changed

docs/rules/no-raw-text.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ since: v0.2.0
1010
1111
- :star: The `"extends": "plugin:@intlify/vue-i18n/recommended"` property in a configuration file enables this rule.
1212

13-
This rule warns the usage of string literal.
13+
This rule warns the usage of literal the bellow:
14+
15+
- string literal
16+
- template literals (no epressions, plain text only)
1417

1518
This rule encourage i18n in about the application needs to be localized.
1619

lib/rules/no-raw-text.ts

Lines changed: 117 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,27 @@ import type {
1111
SourceLocation
1212
} from '../types'
1313

14-
type AnyValue = VAST.ESLintLiteral['value']
14+
type AnyValue =
15+
| VAST.ESLintLiteral['value']
16+
| VAST.ESLintTemplateElement['value']
1517
const config: {
1618
ignorePattern: RegExp
1719
ignoreNodes: string[]
1820
ignoreText: string[]
1921
} = { ignorePattern: /^[^\S\s]$/, ignoreNodes: [], ignoreText: [] }
2022
const hasOnlyWhitespace = (value: string) => /^[\r\n\s\t\f\v]+$/.test(value)
23+
const hasTemplateElementValue = (
24+
value: any // eslint-disable-line @typescript-eslint/no-explicit-any
25+
): value is { raw: string; cooked: string } =>
26+
'raw' in value &&
27+
typeof value.raw === 'string' &&
28+
'cooked' in value &&
29+
typeof value.cooked === 'string'
2130
const INNER_START_OFFSET = '<template>'.length
2231

2332
function calculateLoc(
24-
node: VAST.ESLintLiteral,
25-
base: VAST.ESLintLiteral | null = null
33+
node: VAST.ESLintLiteral | VAST.ESLintTemplateElement,
34+
base: VAST.ESLintLiteral | VAST.ESLintTemplateElement | null = null
2635
) {
2736
return !base
2837
? node.loc
@@ -40,15 +49,24 @@ function calculateLoc(
4049
}
4150
}
4251

43-
function testValue(value: AnyValue) {
52+
function testTextable(value: string): boolean {
4453
return (
45-
typeof value !== 'string' ||
4654
hasOnlyWhitespace(value) ||
4755
config.ignorePattern.test(value.trim()) ||
4856
config.ignoreText.includes(value.trim())
4957
)
5058
}
5159

60+
function testValue(value: AnyValue): boolean {
61+
if (typeof value === 'string') {
62+
return testTextable(value)
63+
} else if (hasTemplateElementValue(value)) {
64+
return testTextable(value.raw)
65+
} else {
66+
return false
67+
}
68+
}
69+
5270
// parent is directive (e.g <p v-xxx="..."></p>)
5371
function checkVAttributeDirective(
5472
context: RuleContext,
@@ -65,63 +83,25 @@ function checkVAttributeDirective(
6583
(attrNode.key.name === 'text' ||
6684
// for vue-eslint-parser v6+
6785
attrNode.key.name.name === 'text') &&
68-
node.expression &&
69-
node.expression.type === 'Literal'
86+
node.expression
7087
) {
71-
const literalNode = node.expression
72-
const value = literalNode.value
73-
74-
if (testValue(value)) {
75-
return
76-
}
77-
78-
const loc = calculateLoc(literalNode, baseNode)
79-
context.report({
80-
loc,
81-
message: `raw text '${literalNode.value}' is used`
82-
})
88+
checkExpressionContainerText(context, node.expression, baseNode)
8389
}
8490
}
8591
}
8692

87-
function checkVExpressionContainerText(
93+
function checkVExpressionContainer(
8894
context: RuleContext,
8995
node: VAST.VExpressionContainer,
90-
baseNode: VAST.ESLintLiteral | null = null
96+
baseNode: VAST.ESLintLiteral | VAST.ESLintTemplateElement | null = null
9197
) {
9298
if (!node.expression) {
9399
return
94100
}
95101

96102
if (node.parent && node.parent.type === 'VElement') {
97103
// parent is element (e.g. <p>{{ ... }}</p>)
98-
if (node.expression.type === 'Literal') {
99-
const literalNode = node.expression
100-
if (testValue(literalNode.value)) {
101-
return
102-
}
103-
104-
const loc = calculateLoc(literalNode, baseNode)
105-
context.report({
106-
loc,
107-
message: `raw text '${literalNode.value}' is used`
108-
})
109-
} else if (node.expression.type === 'ConditionalExpression') {
110-
const targets = [node.expression.consequent, node.expression.alternate]
111-
targets.forEach(target => {
112-
if (target.type === 'Literal') {
113-
if (testValue(target.value)) {
114-
return
115-
}
116-
117-
const loc = calculateLoc(target, baseNode)
118-
context.report({
119-
loc,
120-
message: `raw text '${target.value}' is used`
121-
})
122-
}
123-
})
124-
}
104+
checkExpressionContainerText(context, node.expression, baseNode)
125105
} else if (
126106
node.parent &&
127107
node.parent.type === 'VAttribute' &&
@@ -135,6 +115,67 @@ function checkVExpressionContainerText(
135115
)
136116
}
137117
}
118+
function checkExpressionContainerText(
119+
context: RuleContext,
120+
expression: Exclude<VAST.VExpressionContainer['expression'], null>,
121+
baseNode: VAST.ESLintLiteral | VAST.ESLintTemplateElement | null = null
122+
) {
123+
if (expression.type === 'Literal') {
124+
const literalNode = expression
125+
if (testValue(literalNode.value)) {
126+
return
127+
}
128+
129+
const loc = calculateLoc(literalNode, baseNode)
130+
context.report({
131+
loc,
132+
message: `raw text '${literalNode.value}' is used`
133+
})
134+
} else if (
135+
expression.type === 'TemplateLiteral' &&
136+
expression.expressions.length === 0
137+
) {
138+
const templateNode = expression.quasis[0]
139+
if (testValue(templateNode.value)) {
140+
return
141+
}
142+
143+
const loc = calculateLoc(templateNode, baseNode)
144+
context.report({
145+
loc,
146+
message: `raw text '${templateNode.value.raw}' is used`
147+
})
148+
} else if (expression.type === 'ConditionalExpression') {
149+
const targets = [expression.consequent, expression.alternate]
150+
targets.forEach(target => {
151+
if (target.type === 'Literal') {
152+
if (testValue(target.value)) {
153+
return
154+
}
155+
156+
const loc = calculateLoc(target, baseNode)
157+
context.report({
158+
loc,
159+
message: `raw text '${target.value}' is used`
160+
})
161+
} else if (
162+
target.type === 'TemplateLiteral' &&
163+
target.expressions.length === 0
164+
) {
165+
const node = target.quasis[0]
166+
if (testValue(node.value)) {
167+
return
168+
}
169+
170+
const loc = calculateLoc(node, baseNode)
171+
context.report({
172+
loc,
173+
message: `raw text '${node.value.raw}' is used`
174+
})
175+
}
176+
})
177+
}
178+
}
138179

139180
function checkRawText(
140181
context: RuleContext,
@@ -158,7 +199,7 @@ function findVariable(variables: Variable[], name: string) {
158199
function getComponentTemplateValueNode(
159200
context: RuleContext,
160201
node: VAST.ESLintObjectExpression
161-
): VAST.ESLintLiteral | null {
202+
): VAST.ESLintLiteral | VAST.ESLintTemplateElement | null {
162203
const templateNode = node.properties.find(
163204
(p): p is VAST.ESLintProperty =>
164205
p.type === 'Property' &&
@@ -169,6 +210,11 @@ function getComponentTemplateValueNode(
169210
if (templateNode) {
170211
if (templateNode.value.type === 'Literal') {
171212
return templateNode.value
213+
} else if (
214+
templateNode.value.type === 'TemplateLiteral' &&
215+
templateNode.value.expressions.length === 0
216+
) {
217+
return templateNode.value.quasis[0]
172218
} else if (templateNode.value.type === 'Identifier') {
173219
const templateVariable = findVariable(
174220
context.getScope().variables,
@@ -177,8 +223,15 @@ function getComponentTemplateValueNode(
177223
if (templateVariable) {
178224
const varDeclNode = templateVariable.defs[0]
179225
.node as VAST.ESLintVariableDeclarator
180-
if (varDeclNode.init && varDeclNode.init.type === 'Literal') {
181-
return varDeclNode.init
226+
if (varDeclNode.init) {
227+
if (varDeclNode.init.type === 'Literal') {
228+
return varDeclNode.init
229+
} else if (
230+
varDeclNode.init.type === 'TemplateLiteral' &&
231+
varDeclNode.init.expressions.length === 0
232+
) {
233+
return varDeclNode.init.quasis[0]
234+
}
182235
}
183236
}
184237
}
@@ -188,7 +241,17 @@ function getComponentTemplateValueNode(
188241
}
189242

190243
function getComponentTemplateNode(value: AnyValue) {
191-
return parse(`<template>${value}</template>`, {}).templateBody!
244+
return parse(
245+
`<template>${
246+
// prettier-ignore
247+
typeof value === 'string'
248+
? value
249+
: hasTemplateElementValue(value)
250+
? value.raw
251+
: value
252+
}</template>`,
253+
{}
254+
).templateBody!
192255
}
193256

194257
function create(context: RuleContext): RuleListener {
@@ -213,7 +276,7 @@ function create(context: RuleContext): RuleListener {
213276
{
214277
// template block
215278
VExpressionContainer(node: VAST.VExpressionContainer) {
216-
checkVExpressionContainerText(context, node)
279+
checkVExpressionContainer(context, node)
217280
},
218281

219282
VText(node: VAST.VText) {
@@ -238,7 +301,7 @@ function create(context: RuleContext): RuleListener {
238301
if (node.type === 'VText') {
239302
checkRawText(context, node.value, valueNode.loc)
240303
} else if (node.type === 'VExpressionContainer') {
241-
checkVExpressionContainerText(context, node, valueNode)
304+
checkVExpressionContainer(context, node, valueNode)
242305
}
243306
},
244307
leaveNode() {

0 commit comments

Comments
 (0)