Skip to content

Commit 09ee649

Browse files
committed
⭐ new(rule): add no-unused-key rule
1 parent df65fb8 commit 09ee649

27 files changed

+1340
-166
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Details changes for each release are documented in the [CHANGELOG.md](https://gi
1818
## :white_check_mark: TODO
1919
- [x] no-missing-key
2020
- [ ] no-dynamic-key
21-
- [ ] no-unused-key
21+
- [x] no-unused-key
2222
- [ ] no-raw-text
2323
- [ ] valid-message-syntax
2424
- [ ] keys-order

docs/.vuepress/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @fileoverview VuePress configration
3+
* @author kazuya kawaguchi (a.k.a. kazupon)
4+
*/
15
'use strict'
26

37
const { withCategories } = require('../../scripts/lib/rules')

docs/rules/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@
99
|:--------|:------------|:---|
1010
| [vue-i18n/<wbr>no-missing-key](./no-missing-key.html) | disallow missing locale message key at localization methods | :star: |
1111

12+
## Best Practices
13+
14+
| Rule ID | Description | |
15+
|:--------|:------------|:---|
16+
| [vue-i18n/<wbr>no-unused-key](./no-unused-key.html) | disallow unused localization keys | |
17+

docs/rules/no-unused-key.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# vue-i18n/no-unused-key
2+
3+
> disallow unused localization keys
4+
5+
Localization keys that not used anywhere in the code are most likely an error due to incomplete refactoring. Such localization keys take up code size and can lead to confusion by readers.
6+
7+
## :book: Rule Details
8+
9+
This rule is aimed at eliminating unused localization keys.
10+
11+
:-1: Examples of **incorrect** code for this rule:
12+
13+
locale messages:
14+
```js
15+
// ✗ BAD
16+
{
17+
"hello": "Hello! DIO!",
18+
"hi": "Hi! DIO!" // not used in application
19+
}
20+
```
21+
22+
In localization codes of application:
23+
24+
```vue
25+
<template>
26+
<div class="app">
27+
<p>{{ $t('hello') }}</p>
28+
</div>
29+
</template>
30+
```
31+
32+
```js
33+
const i18n = new VueI18n({
34+
locale: 'en',
35+
messages: {
36+
en: require('./locales/en.json')
37+
}
38+
})
39+
40+
i18n.t('hello')
41+
```
42+
43+
:+1: Examples of **correct** code for this rule:
44+
45+
locale messages:
46+
// ✓ GOOD
47+
```js
48+
{
49+
"hello": "Hello! DIO!",
50+
"hi": "Hi! DIO!"
51+
}
52+
```
53+
54+
In localization codes of application:
55+
56+
```vue
57+
<template>
58+
<div class="app">
59+
<p>{{ $t('hello') }}</p>
60+
</div>
61+
</template>
62+
```
63+
64+
```js
65+
const i18n = new VueI18n({
66+
locale: 'en',
67+
messages: {
68+
en: require('./locales/en.json')
69+
}
70+
})
71+
72+
i18n.t('hi')
73+
```
74+
75+
## Options
76+
77+
You can specify allowed directive-comments.
78+
79+
```json
80+
{
81+
"vue-i18n/no-unused-key": ["error", {
82+
"extensions": [".js", ".vue"]
83+
}]
84+
}
85+
```
86+
87+
- `extenstions`: an array to allow specified lintable target file extention. If you don't set any options, it set to `.js` and` .vue` as default.

lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
module.exports = {
88
configs: require('./configs'),
99
rules: require('./rules'),
10-
processors: {}
10+
processors: require('./processors')
1111
}

lib/processors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** DON'T EDIT THIS FILE; was created by scripts. */
2+
'use strict'
3+
4+
module.exports = {
5+
'.json': require('./processors/json')
6+
}

lib/processors/.gitkeep

Whitespace-only changes.

lib/processors/json.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @fileoverview JSON processor
3+
* @author kazuya kawaguchi (a.k.a. kazupon)
4+
*/
5+
'use strict'
6+
7+
const localeMessageFiles = {}
8+
9+
module.exports = {
10+
preprocess (text, filename) {
11+
localeMessageFiles[filename] = text
12+
// JSON into a JavaScript comment
13+
const textBuf = Buffer.from(text.trim())
14+
const filenameBuf = Buffer.from(filename)
15+
return [`/*${textBuf.toString('base64')}*//*${filenameBuf.toString('base64')}*/\n`]
16+
},
17+
18+
postprocess ([errors], filename) {
19+
delete localeMessageFiles[filename]
20+
return [...errors.filter(
21+
error => !error.ruleId || error.ruleId === 'vue-i18n/no-unused-key'
22+
)]
23+
},
24+
25+
supportsAutofix: true
26+
}

lib/rules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
'use strict'
33

44
module.exports = {
5-
'no-missing-key': require('./rules/no-missing-key')
5+
'no-missing-key': require('./rules/no-missing-key'),
6+
'no-unused-key': require('./rules/no-unused-key')
67
}

lib/rules/no-unused-key.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const { extname } = require('path')
7+
const jsonAstParse = require('json-to-ast')
8+
const jsonDiffPatch = require('jsondiffpatch').create({})
9+
const flatten = require('flat')
10+
const collectKeys = require('../utils/collect-keys')
11+
const { loadLocaleMessages } = require('../utils/index')
12+
const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-key')
13+
14+
let usedLocaleMessageKeys = null // used locale message keys
15+
let localeMessages = null // used locale messages
16+
17+
function findExistLocaleMessage (fullpath, localeMessages) {
18+
return localeMessages.find(message => message.fullpath === fullpath)
19+
}
20+
21+
function getUnusedKeys (diffLocaleMessage) {
22+
const unusedKeys = []
23+
Object.keys(diffLocaleMessage).forEach(key => {
24+
const value = diffLocaleMessage[key]
25+
if (value && Array.isArray(value) && value.length === 1) {
26+
unusedKeys.push(key)
27+
}
28+
})
29+
return unusedKeys
30+
}
31+
32+
function traverseJsonAstWithUnusedKeys (unusedKeys, ast, fn) {
33+
unusedKeys.forEach(key => {
34+
const fullpath = String(key)
35+
const paths = key.split('.')
36+
traverseNode(fullpath, paths, ast, fn)
37+
})
38+
}
39+
40+
function traverseNode (fullpath, paths, ast, fn) {
41+
const path = paths.shift()
42+
if (ast.type === 'Object' && ast.children.length > 0) {
43+
ast.children.forEach(child => {
44+
if (child.type === 'Property') {
45+
const key = child.key
46+
if (key.type === 'Identifier' && key.value === path) {
47+
const value = child.value
48+
if (value.type === 'Object') {
49+
return traverseNode(fullpath, paths, value, fn)
50+
} else {
51+
return fn(fullpath, key)
52+
}
53+
}
54+
}
55+
})
56+
}
57+
}
58+
59+
function create (context) {
60+
const filename = context.getFilename()
61+
if (extname(filename) !== '.json') {
62+
debug(`ignore ${filename} in no-unused-key`)
63+
return {}
64+
}
65+
66+
const { settings } = context
67+
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
68+
// TODO: should be error
69+
return {}
70+
}
71+
localeMessages = localeMessages || loadLocaleMessages(settings['vue-i18n'].localeDir)
72+
73+
const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages)
74+
if (!targetLocaleMessage) {
75+
debug(`ignore ${filename} in no-unused-key`)
76+
return {}
77+
}
78+
79+
const { extensions } = (context.options && context.options[0]) || { extensions: ['.js', '.vue'] }
80+
const src = [process.cwd()] || ['.']
81+
82+
if (!usedLocaleMessageKeys) {
83+
usedLocaleMessageKeys = collectKeys(src, extensions)
84+
}
85+
86+
return {
87+
Program (node) {
88+
const [stringNode, filenameNode] = node.comments
89+
const jsonString = Buffer.from(stringNode.value, 'base64').toString()
90+
const jsonFilename = Buffer.from(filenameNode.value, 'base64').toString()
91+
const jsonValue = JSON.parse(jsonString)
92+
try {
93+
const diffValue = jsonDiffPatch.diff(
94+
flatten(usedLocaleMessageKeys, { safe: true }),
95+
flatten(jsonValue, { safe: true })
96+
)
97+
const diffLocaleMessage = flatten(diffValue, { safe: true })
98+
const unusedKeys = getUnusedKeys(diffLocaleMessage)
99+
100+
const jsonAstSettings = { loc: true, source: jsonFilename }
101+
const ast = jsonAstParse(jsonString, jsonAstSettings)
102+
traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {
103+
const { line, column } = node.loc.start
104+
context.report({
105+
message: `unused '${fullpath}' key in '${targetLocaleMessage.path}'`,
106+
loc: { line, column }
107+
})
108+
})
109+
} catch ({ message, line, column }) {
110+
context.report({
111+
message,
112+
loc: { line, column }
113+
})
114+
}
115+
}
116+
}
117+
}
118+
119+
module.exports = {
120+
meta: {
121+
type: 'suggestion',
122+
docs: {
123+
description: 'disallow unused localization keys',
124+
category: 'Best Practices',
125+
recommended: false
126+
},
127+
fixable: false,
128+
schema: [{
129+
type: 'object',
130+
properties: {
131+
extensions: {
132+
type: 'array',
133+
items: { type: 'string' },
134+
default: ['.js', '.vue']
135+
}
136+
},
137+
additionalProperties: false
138+
}]
139+
},
140+
create
141+
}

0 commit comments

Comments
 (0)