Skip to content

Commit cba0311

Browse files
authored
Fix false positives and false negatives for no-missing-keys and no-unused-keys rules (#302)
1 parent ab2e15b commit cba0311

File tree

7 files changed

+127
-89
lines changed

7 files changed

+127
-89
lines changed

lib/rules/no-dynamic-keys.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,11 @@
11
/**
22
* @author kazuya kawaguchi (a.k.a. kazupon)
33
*/
4-
import { defineTemplateBodyVisitor } from '../utils/index'
4+
import { defineTemplateBodyVisitor, isStaticLiteral } from '../utils/index'
55
import type { RuleContext, RuleListener } from '../types'
66
import type { AST as VAST } from 'vue-eslint-parser'
77
import { createRule } from '../utils/rule'
88

9-
function isStatic(
10-
node:
11-
| VAST.ESLintExpression
12-
| VAST.ESLintSpreadElement
13-
| VAST.VFilterSequenceExpression
14-
| VAST.VForExpression
15-
| VAST.VOnExpression
16-
| VAST.VSlotScopeExpression
17-
): boolean {
18-
if (node.type === 'Literal') {
19-
return true
20-
}
21-
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
22-
return true
23-
}
24-
return false
25-
}
26-
279
function getNodeName(context: RuleContext, node: VAST.Node): string {
2810
if (node.type === 'Identifier') {
2911
return node.name
@@ -50,7 +32,7 @@ function checkDirective(context: RuleContext, node: VAST.VDirective) {
5032
node.value &&
5133
node.value.type === 'VExpressionContainer' &&
5234
node.value.expression &&
53-
!isStatic(node.value.expression)
35+
!isStaticLiteral(node.value.expression)
5436
) {
5537
const name = getNodeName(context, node.value.expression)
5638
context.report({
@@ -70,7 +52,7 @@ function checkComponent(context: RuleContext, node: VAST.VDirectiveKey) {
7052
node.parent.value &&
7153
node.parent.value.type === 'VExpressionContainer' &&
7254
node.parent.value.expression &&
73-
!isStatic(node.parent.value.expression)
55+
!isStaticLiteral(node.parent.value.expression)
7456
) {
7557
const name = getNodeName(context, node.parent.value.expression)
7658
context.report({
@@ -100,7 +82,7 @@ function checkCallExpression(
10082
}
10183

10284
const [keyNode] = node.arguments
103-
if (!isStatic(keyNode)) {
85+
if (!isStaticLiteral(keyNode)) {
10486
const name = getNodeName(context, keyNode)
10587
context.report({
10688
node,

lib/rules/no-missing-keys.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/**
22
* @author kazuya kawaguchi (a.k.a. kazupon)
33
*/
4-
import { defineTemplateBodyVisitor, getLocaleMessages } from '../utils/index'
4+
import {
5+
defineTemplateBodyVisitor,
6+
getLocaleMessages,
7+
getStaticLiteralValue,
8+
isStaticLiteral
9+
} from '../utils/index'
510
import type { AST as VAST } from 'vue-eslint-parser'
611
import type { RuleContext, RuleListener } from '../types'
712
import { createRule } from '../utils/rule'
@@ -45,10 +50,9 @@ function checkDirective(context: RuleContext, node: VAST.VDirective) {
4550
if (
4651
node.value &&
4752
node.value.type === 'VExpressionContainer' &&
48-
node.value.expression &&
49-
node.value.expression.type === 'Literal'
53+
isStaticLiteral(node.value.expression)
5054
) {
51-
const key = node.value.expression.value
55+
const key = getStaticLiteralValue(node.value.expression)
5256
if (!key) {
5357
// TODO: should be error
5458
return
@@ -111,11 +115,11 @@ function checkCallExpression(
111115
}
112116

113117
const [keyNode] = node.arguments
114-
if (keyNode.type !== 'Literal') {
118+
if (!isStaticLiteral(keyNode)) {
115119
return
116120
}
117121

118-
const key = keyNode.value
122+
const key = getStaticLiteralValue(keyNode)
119123
if (!key) {
120124
// TODO: should be error
121125
return

lib/rules/no-raw-text.ts

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import { parse, AST as VAST } from 'vue-eslint-parser'
55
import type { AST as JSONAST } from 'jsonc-eslint-parser'
66
import { parseJSON, getStaticJSONValue } from 'jsonc-eslint-parser'
7+
import type { StaticLiteral } from '../utils/index'
78
import {
9+
getStaticLiteralValue,
10+
isStaticLiteral,
811
defineTemplateBodyVisitor,
912
getLocaleMessages,
1013
getStaticAttributes,
@@ -28,11 +31,7 @@ import { createRule } from '../utils/rule'
2831
import { toRegExp } from '../utils/regexp'
2932

3033
type LiteralValue = VAST.ESLintLiteral['value']
31-
type StaticTemplateLiteral = VAST.ESLintTemplateLiteral & {
32-
quasis: [VAST.ESLintTemplateElement]
33-
expressions: [/* empty */]
34-
}
35-
type TemplateOptionValueNode = VAST.ESLintLiteral | StaticTemplateLiteral
34+
type TemplateOptionValueNode = StaticLiteral
3635
type NodeScope = 'template' | 'template-option' | 'jsx'
3736
type TargetAttrs = { name: RegExp; attrs: Set<string> }
3837
type Config = {
@@ -76,24 +75,8 @@ function getTargetAttrs(tagName: string, config: Config): Set<string> {
7675
return new Set(result)
7776
}
7877

79-
function isStaticTemplateLiteral(
80-
node:
81-
| VAST.ESLintExpression
82-
| VAST.VExpressionContainer['expression']
83-
| VAST.ESLintPattern
84-
): node is StaticTemplateLiteral {
85-
return Boolean(
86-
node && node.type === 'TemplateLiteral' && node.expressions.length === 0
87-
)
88-
}
8978
function calculateRange(
90-
node:
91-
| VAST.ESLintLiteral
92-
| StaticTemplateLiteral
93-
| VAST.VText
94-
| JSXText
95-
| VAST.VLiteral
96-
| VAST.VIdentifier,
79+
node: StaticLiteral | VAST.VText | JSXText | VAST.VLiteral | VAST.VIdentifier,
9780
base: TemplateOptionValueNode | null
9881
): Range {
9982
const range = node.range
@@ -104,12 +87,7 @@ function calculateRange(
10487
return [offset + range[0], offset + range[1]]
10588
}
10689
function calculateLoc(
107-
node:
108-
| VAST.ESLintLiteral
109-
| StaticTemplateLiteral
110-
| VAST.VText
111-
| JSXText
112-
| VAST.VLiteral,
90+
node: StaticLiteral | VAST.VText | JSXText | VAST.VLiteral,
11391
base: TemplateOptionValueNode | null,
11492
context: RuleContext
11593
) {
@@ -209,16 +187,12 @@ function checkExpressionContainerText(
209187
baseNode: TemplateOptionValueNode | null,
210188
scope: NodeScope
211189
) {
212-
if (expression.type === 'Literal') {
213-
checkLiteral(context, expression, config, baseNode, scope)
214-
} else if (isStaticTemplateLiteral(expression)) {
190+
if (isStaticLiteral(expression)) {
215191
checkLiteral(context, expression, config, baseNode, scope)
216192
} else if (expression.type === 'ConditionalExpression') {
217193
const targets = [expression.consequent, expression.alternate]
218194
targets.forEach(target => {
219-
if (target.type === 'Literal') {
220-
checkLiteral(context, target, config, baseNode, scope)
221-
} else if (isStaticTemplateLiteral(target)) {
195+
if (isStaticLiteral(target)) {
222196
checkLiteral(context, target, config, baseNode, scope)
223197
}
224198
})
@@ -227,15 +201,12 @@ function checkExpressionContainerText(
227201

228202
function checkLiteral(
229203
context: RuleContext,
230-
literal: VAST.ESLintLiteral | StaticTemplateLiteral,
204+
literal: StaticLiteral,
231205
config: Config,
232206
baseNode: TemplateOptionValueNode | null,
233207
scope: NodeScope
234208
) {
235-
const value =
236-
literal.type !== 'TemplateLiteral'
237-
? literal.value
238-
: literal.quasis[0].value.cooked
209+
const value = getStaticLiteralValue(literal)
239210

240211
if (testValue(value, config)) {
241212
return
@@ -465,9 +436,7 @@ function getComponentTemplateValueNode(
465436
)
466437

467438
if (templateNode) {
468-
if (templateNode.value.type === 'Literal') {
469-
return templateNode.value
470-
} else if (isStaticTemplateLiteral(templateNode.value)) {
439+
if (isStaticLiteral(templateNode.value)) {
471440
return templateNode.value
472441
} else if (templateNode.value.type === 'Identifier') {
473442
const templateVariable = findVariable(
@@ -478,9 +447,7 @@ function getComponentTemplateValueNode(
478447
const varDeclNode = templateVariable.defs[0]
479448
.node as VAST.ESLintVariableDeclarator
480449
if (varDeclNode.init) {
481-
if (varDeclNode.init.type === 'Literal') {
482-
return varDeclNode.init
483-
} else if (isStaticTemplateLiteral(varDeclNode.init)) {
450+
if (isStaticLiteral(varDeclNode.init)) {
484451
return varDeclNode.init
485452
}
486453
}
@@ -492,12 +459,8 @@ function getComponentTemplateValueNode(
492459
}
493460

494461
function getComponentTemplateNode(node: TemplateOptionValueNode) {
495-
return parse(
496-
`<template>${
497-
node.type === 'TemplateLiteral' ? node.quasis[0].value.cooked : node.value
498-
}</template>`,
499-
{}
500-
).templateBody!
462+
return parse(`<template>${getStaticLiteralValue(node)}</template>`, {})
463+
.templateBody!
501464
}
502465

503466
function withoutEscape(
@@ -508,10 +471,7 @@ function withoutEscape(
508471
return false
509472
}
510473
const sourceText = context.getSourceCode().getText(baseNode).slice(1, -1)
511-
const templateText =
512-
baseNode.type === 'TemplateLiteral'
513-
? baseNode.quasis[0].value.cooked
514-
: `${baseNode.value}`
474+
const templateText = `${getStaticLiteralValue(baseNode)}`
515475
return sourceText === templateText
516476
}
517477

lib/utils/collect-keys.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { RuleContext, VisitorKeys } from '../types'
1515
// @ts-expect-error -- ignore
1616
import { Legacy } from '@eslint/eslintrc'
1717
import { getCwd } from './get-cwd'
18+
import { isStaticLiteral, getStaticLiteralValue } from './index'
1819
const debug = debugBuilder('eslint-plugin-vue-i18n:collect-keys')
1920
const { CascadingConfigArrayFactory } = Legacy
2021

@@ -39,11 +40,11 @@ function getKeyFromCallExpression(node: VAST.ESLintCallExpression) {
3940
}
4041

4142
const [keyNode] = node.arguments
42-
if (keyNode.type !== 'Literal') {
43+
if (!isStaticLiteral(keyNode)) {
4344
return null
4445
}
4546

46-
return keyNode.value ? keyNode.value : null
47+
return getStaticLiteralValue(keyNode)
4748
}
4849

4950
/**
@@ -53,10 +54,9 @@ function getKeyFromVDirective(node: VAST.VDirective) {
5354
if (
5455
node.value &&
5556
node.value.type === 'VExpressionContainer' &&
56-
node.value.expression &&
57-
node.value.expression.type === 'Literal'
57+
isStaticLiteral(node.value.expression)
5858
) {
59-
return node.value.expression.value ? node.value.expression.value : null
59+
return getStaticLiteralValue(node.value.expression)
6060
} else {
6161
return null
6262
}

lib/utils/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ export function getDirective(
107107
)
108108
}
109109

110+
export type StaticLiteral = VAST.ESLintLiteral | VAST.ESLintTemplateLiteral
111+
export function isStaticLiteral(node: VAST.Node | null): node is StaticLiteral {
112+
return Boolean(
113+
node &&
114+
(node.type === 'Literal' ||
115+
(node.type === 'TemplateLiteral' && node.expressions.length === 0))
116+
)
117+
}
118+
export function getStaticLiteralValue(
119+
node: StaticLiteral
120+
): VAST.ESLintLiteral['value'] {
121+
return node.type !== 'TemplateLiteral'
122+
? node.value
123+
: node.quasis[0].value.cooked || node.quasis[0].value.raw
124+
}
125+
110126
function loadLocaleMessages(
111127
localeFilesList: LocaleFiles[],
112128
cwd: string

tests/lib/rules/no-missing-keys.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function buildTestsForLocales<
4646

4747
const tester = new RuleTester({
4848
parser: require.resolve('vue-eslint-parser'),
49-
parserOptions: { ecmaVersion: 2015 }
49+
parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
5050
})
5151

5252
tester.run('no-missing-keys', rule as never, {
@@ -193,6 +193,27 @@ tester.run('no-missing-keys', rule as never, {
193193
<script>
194194
t('foo.bar')
195195
</script>`
196+
},
197+
{
198+
// template literal
199+
filename: 'test.vue',
200+
code: `
201+
<i18n locale="en">
202+
{ "foo": "foo", "bar": "bar", "baz": "baz" }
203+
</i18n>
204+
<template>
205+
<div id="app">
206+
{{ $t(\`foo\`) }}
207+
</div>
208+
<div v-t="\`baz\`"/>
209+
</template>
210+
<script>
211+
export default {
212+
created () {
213+
this.$t(\`bar\`)
214+
}
215+
}
216+
</script>`
196217
}
197218
]
198219
),
@@ -326,6 +347,41 @@ tester.run('no-missing-keys', rule as never, {
326347
<i18n-t keypath="hi"></i18n-t>
327348
</template>`,
328349
errors: [`'hi' does not exist in localization message resources`]
350+
},
351+
{
352+
// template literal
353+
filename: 'test.vue',
354+
code: `
355+
<i18n locale="en">
356+
{ }
357+
</i18n>
358+
<template>
359+
<div id="app">
360+
{{ $t(\`foo\`) }}
361+
</div>
362+
<div v-t="\`baz\`"/>
363+
</template>
364+
<script>
365+
export default {
366+
created () {
367+
this.$t(\`bar\`)
368+
}
369+
}
370+
</script>`,
371+
errors: [
372+
{
373+
message: "'foo' does not exist in localization message resources",
374+
line: 7
375+
},
376+
{
377+
message: "'baz' does not exist in localization message resources",
378+
line: 9
379+
},
380+
{
381+
message: "'bar' does not exist in localization message resources",
382+
line: 14
383+
}
384+
]
329385
}
330386
]
331387
)

0 commit comments

Comments
 (0)