Skip to content

Commit 00ec913

Browse files
authored
Fix false positives for keys linked in no-unused-keys rule (#76)
1 parent 21017fd commit 00ec913

File tree

9 files changed

+167
-21
lines changed

9 files changed

+167
-21
lines changed

lib/rules/no-unused-keys.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { extname } = require('path')
77
const jsonDiffPatch = require('jsondiffpatch').create({})
88
const flatten = require('flat')
99
const collectKeys = require('../utils/collect-keys')
10+
const collectLinkedKeys = require('../utils/collect-linked-keys')
1011
const {
1112
UNEXPECTED_ERROR_LOCATION,
1213
getLocaleMessages,
@@ -19,26 +20,34 @@ const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-keys')
1920
* @typedef {import('../utils/locale-messages').LocaleMessage} LocaleMessage
2021
*/
2122

23+
/** @type {string[] | null} */
2224
let usedLocaleMessageKeys = null // used locale message keys
2325

2426
/**
2527
* @param {RuleContext} context
2628
* @param {LocaleMessage} targetLocaleMessage
2729
* @param {string} json
28-
* @param {object} usedkeys
30+
* @param {string[]} usedkeys
2931
*/
3032
function getUnusedKeys (context, targetLocaleMessage, json, usedkeys) {
3133
let unusedKeys = []
3234

3335
try {
34-
let compareKeys = { ...usedkeys }
36+
const jsonValue = JSON.parse(json)
37+
38+
let compareKeys = [
39+
...usedkeys,
40+
...collectLinkedKeys(jsonValue)
41+
].reduce((values, current) => {
42+
values[current] = true
43+
return values
44+
}, {})
3545
if (targetLocaleMessage.localeKey === 'key') {
3646
compareKeys = targetLocaleMessage.locales.reduce((keys, locale) => {
37-
keys[locale] = usedkeys
47+
keys[locale] = compareKeys
3848
return keys
3949
}, {})
4050
}
41-
const jsonValue = JSON.parse(json)
4251
const diffValue = jsonDiffPatch.diff(
4352
flatten(compareKeys, { safe: true }),
4453
flatten(jsonValue, { safe: true })

lib/utils/collect-keys.js

+14-10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ function processText (text, config, filename, linter) {
8686
).filter(e => e)
8787
}
8888

89+
/**
90+
* Collect the used keys.
91+
* @returns {string[]}
92+
*/
8993
function collectKeys (files, extensions) {
9094
debug('collectKeys', files, extensions)
9195

@@ -107,22 +111,22 @@ function collectKeys (files, extensions) {
107111
linter.defineParser('vue-eslint-parser', require(require.resolve(config.parser)))
108112
linter.defineRule(INTERNAL_RULE_KEY, { create })
109113

114+
const results = new Set()
115+
110116
// detect used lodalization keys with linter
111-
const fileList = listFilesToProcess(files, config)
112-
const results = fileList.map(fileInfo => {
113-
const { filename, ignored } = fileInfo
117+
for (const { filename, ignored } of listFilesToProcess(files, config)) {
114118
debug(`Processing file ... ${filename}`)
115119

116-
if (ignored) { return {} }
120+
if (ignored) { continue }
117121

118122
const text = readFileSync(resolve(filename), 'utf8')
119-
return processText(text, config, filename, linter)
120-
}).reduce((values, current) => values.concat(current), [])
121123

122-
return results.reduce((values, current) => {
123-
values[current] = true
124-
return values
125-
}, {})
124+
for (const usedKey of processText(text, config, filename, linter)) {
125+
results.add(usedKey)
126+
}
127+
}
128+
129+
return [...results]
126130
}
127131

128132
module.exports = collectKeys

lib/utils/collect-linked-keys.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @fileoverview Collect the keys used by the linked messages.
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
// Note: If vue-i18n@next parser is separated from vue plugin, change it to use that.
8+
9+
const linkKeyMatcher = /(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g
10+
const linkKeyPrefixMatcher = /^@(?:\.([a-z]+))?:/
11+
const bracketsMatcher = /[()]/g
12+
13+
/**
14+
* Extract the keys used by the linked messages.
15+
* @param {any} object
16+
* @returns {IterableIterator<string>}
17+
*/
18+
function * extractUsedKeysFromLinks (object) {
19+
for (const value of Object.values(object)) {
20+
if (!value) {
21+
continue
22+
}
23+
if (typeof value === 'object') {
24+
yield * extractUsedKeysFromLinks(value)
25+
} else if (typeof value === 'string') {
26+
if (value.indexOf('@:') >= 0 || value.indexOf('@.') >= 0) {
27+
// see https://github.com/kazupon/vue-i18n/blob/c07d1914dcac186291b658a8b9627732010f6848/src/index.js#L435
28+
const matches = value.match(linkKeyMatcher)
29+
for (const idx in matches) {
30+
const link = matches[idx]
31+
const linkKeyPrefixMatches = link.match(linkKeyPrefixMatcher)
32+
const [linkPrefix] = linkKeyPrefixMatches
33+
34+
// Remove the leading @:, @.case: and the brackets
35+
const linkPlaceholder = link
36+
.replace(linkPrefix, '')
37+
.replace(bracketsMatcher, '')
38+
yield linkPlaceholder
39+
}
40+
}
41+
}
42+
}
43+
}
44+
45+
/**
46+
* Collect the keys used by the linked messages.
47+
* @param {any} object
48+
* @returns {string[]}
49+
*/
50+
module.exports = function collectLinkedKeys (object) {
51+
return [...new Set(extractUsedKeysFromLinks(object))]
52+
}

tests/fixtures/no-unused-keys/valid/constructor-option-format/locales/index.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
"en": {
33
"hello": "hello world",
44
"messages": {
5-
"hello": "hi DIO!"
5+
"hello": "hi DIO!",
6+
"link": "@:messages.hello"
67
},
78
"hello_dio": "hello underscore DIO!",
89
"hello {name}": "hello {name}!"
910
},
1011
"ja": {
1112
"hello": "ハローワールド",
1213
"messages": {
13-
"hello": "こんにちは、DIO!"
14+
"hello": "こんにちは、DIO!",
15+
"link": "@:messages.hello"
1416
},
1517
"hello_dio": "こんにちは、アンダースコア DIO!",
1618
"hello {name}": "こんにちは、{name}!"

tests/fixtures/no-unused-keys/valid/constructor-option-format/src/App.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div id="app">
3-
<p v-t="'hello_dio'">{{ $t('messages.hello') }}</p>
3+
<p v-t="'hello_dio'">{{ $t('messages.link') }}</p>
44
</div>
55
</template>
66

tests/fixtures/no-unused-keys/valid/vue-cli-format/locales/en.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"hello": "hello world",
33
"messages": {
4-
"hello": "hi DIO!"
4+
"hello": "hi DIO!",
5+
"link": "@:messages.hello"
56
},
67
"hello_dio": "hello underscore DIO!",
78
"hello {name}": "hello {name}!"
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"hello": "ハローワールド",
33
"messages": {
4-
"hello": "こんにちは、DIO!"
4+
"hello": "こんにちは、DIO!",
5+
"link": "@:messages.hello"
56
},
67
"hello_dio": "こんにちは、アンダースコア DIO!",
78
"hello {name}": "こんにちは、{name}!"
8-
}
9+
}

tests/fixtures/no-unused-keys/valid/vue-cli-format/src/App.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div id="app">
3-
<p v-t="'hello_dio'">{{ $t('messages.hello') }}</p>
3+
<p v-t="'hello_dio'">{{ $t('messages.link') }}</p>
44
</div>
55
</template>
66

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
const assert = require('assert')
7+
const collectLinkedKeys = require('../../../lib/utils/collect-linked-keys')
8+
9+
describe('collectLinkedKeys', () => {
10+
it('should be get the keys used in the plain linked message.', () => {
11+
const object = {
12+
message: {
13+
the_world: 'the world',
14+
dio: 'DIO:',
15+
linked: '@:message.dio @:message.the_world !!!!'
16+
}
17+
}
18+
19+
const expected = ['message.dio', 'message.the_world']
20+
assert.deepStrictEqual(collectLinkedKeys(object), expected)
21+
})
22+
it('should be get the keys used in the formatting linked message.', () => {
23+
const object = {
24+
message: {
25+
homeAddress: 'Home address',
26+
missingHomeAddress: 'Please provide @.lower:message.homeAddress'
27+
}
28+
}
29+
30+
const expected = ['message.homeAddress']
31+
assert.deepStrictEqual(collectLinkedKeys(object), expected)
32+
})
33+
it('should be get the keys used in the linked message with brackets.', () => {
34+
const object = {
35+
message: {
36+
dio: 'DIO',
37+
linked: 'There\'s a reason, you lost, @:(message.dio).'
38+
}
39+
}
40+
41+
const expected = ['message.dio']
42+
assert.deepStrictEqual(collectLinkedKeys(object), expected)
43+
})
44+
45+
it('should be get the keys used in the linked message.', () => {
46+
const object = {
47+
foo: {
48+
a: 'Hi',
49+
b: '@:foo.a lorem ipsum @:bar.a !!!!',
50+
c: {
51+
a: '@:(bar.a) @:bar.b.a'
52+
},
53+
d: 'Hello'
54+
},
55+
bar: {
56+
a: 'Yes',
57+
b: {
58+
a: '@.lower:foo.d',
59+
// invaid values
60+
b: null,
61+
c: 123,
62+
e: /reg/,
63+
f: () => {},
64+
g: [true, false]
65+
}
66+
}
67+
}
68+
69+
const expected = [
70+
'foo.a',
71+
'bar.a',
72+
'bar.b.a',
73+
'foo.d'
74+
]
75+
assert.deepStrictEqual(collectLinkedKeys(object), expected)
76+
})
77+
})

0 commit comments

Comments
 (0)