Skip to content

Commit d502436

Browse files
authored
Add enableFix option to @intlify/vue-i18n/no-unused-keys rule (#83)
* Add quick fix to remove unused keys to the `@intlify/vue-i18n/no-unused-keys` rule. - Other Updates - Change to use eslint-plugin-jsonc. - Remove json processor as it is no longer needed. - Add `/base` config for testcases. * Add `enableFix` option to `@intlify/vue-i18n/no-unused-keys` rule
1 parent 9cfac17 commit d502436

File tree

25 files changed

+1562
-445
lines changed

25 files changed

+1562
-445
lines changed

docs/rules/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
| Rule ID | Description | |
1818
|:--------|:------------|:---|
1919
| [@intlify/vue-i18n/<wbr>no-dynamic-keys](./no-dynamic-keys.html) | disallow localization dynamic keys at localization methods | |
20-
| [@intlify/vue-i18n/<wbr>no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | |
20+
| [@intlify/vue-i18n/<wbr>no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | :black_nib: |
2121

docs/rules/no-unused-keys.md

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

33
> disallow unused localization keys
44
5+
- :black_nib:️ The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
6+
57
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.
68

79
## :book: Rule Details
@@ -101,10 +103,12 @@ i18n.t('hi')
101103
{
102104
"@intlify/vue-i18n/no-unused-keys": ["error", {
103105
"src": "./src",
104-
"extensions": [".js", ".vue"]
106+
"extensions": [".js", ".vue"],
107+
"enableFix": false
105108
}]
106109
}
107110
```
108111

109112
- `src`: specify the source codes directory to be able to lint. If you don't set any options, it set to `process.cwd()` as default.
110113
- `extensions`: an array to allow specified lintable target file extension. If you don't set any options, it set to `.js` and` .vue` as default.
114+
- `enableFix`: if `true`, enable automatically remove unused keys on `eslint --fix`. If you don't set any options, it set to `false` as default. (This is an experimental feature.)

docs/started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ The most rules of `eslint-plugin-vue-i18n` require `vue-eslint-parser` to check
8989
Make sure you have one of the following settings in your **.eslintrc**:
9090

9191
- `"extends": ["plugin:@intlify/vue-i18n/recommended"]`
92+
- `"extends": ["plugin:@intlify/vue-i18n/base"]`
9293

9394
If you already use other parser (e.g. `"parser": "babel-eslint"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:
9495

lib/configs.js

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

44
module.exports = {
5+
'base': require('./configs/base'),
56
'recommended': require('./configs/recommended')
67
}

lib/configs/base.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict'
2+
3+
module.exports = {
4+
parser: require.resolve('vue-eslint-parser'),
5+
plugins: ['@intlify/vue-i18n'],
6+
overrides: [
7+
{
8+
files: ['*.json', '*.json5'],
9+
// TODO: If you do not use vue-eslint-parser, you will get an error in vue rules.
10+
// see https://github.com/vuejs/eslint-plugin-vue/pull/1262
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: {
13+
parser: require.resolve('eslint-plugin-jsonc')
14+
}
15+
}
16+
]
17+
}

lib/configs/recommended.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'use strict'
33

44
module.exports = {
5-
parser: require.resolve('vue-eslint-parser'),
5+
extends: [require.resolve('./base')],
66
parserOptions: {
77
ecmaVersion: 2018,
88
sourceType: 'module',
@@ -14,7 +14,6 @@ module.exports = {
1414
browser: true,
1515
es6: true
1616
},
17-
plugins: ['@intlify/vue-i18n'],
1817
rules: {
1918
'@intlify/vue-i18n/no-html-messages': 'warn',
2019
'@intlify/vue-i18n/no-missing-keys': 'warn',

lib/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@
66

77
module.exports = {
88
configs: require('./configs'),
9-
rules: require('./rules'),
10-
processors: require('./processors')
9+
rules: require('./rules')
1110
}

lib/processors.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

lib/processors/json.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

lib/rules/no-html-messages.js

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,13 @@
55

66
const { extname } = require('path')
77
const parse5 = require('parse5')
8-
const {
9-
getLocaleMessages,
10-
extractJsonInfo,
11-
generateJsonAst
12-
} = require('../utils/index')
8+
const { getLocaleMessages } = require('../utils/index')
9+
const { traverseNodes } = require('eslint-plugin-jsonc')
1310
const debug = require('debug')('eslint-plugin-vue-i18n:no-html-messages')
1411

15-
function traverseNode (node, fn) {
16-
if (node.type === 'Object' && node.children.length > 0) {
17-
node.children.forEach(child => {
18-
if (child.type === 'Property') {
19-
const keyNode = child.key
20-
const valueNode = child.value
21-
if (keyNode.type === 'Identifier' && valueNode.type === 'Object') {
22-
return traverseNode(valueNode, fn)
23-
} else {
24-
return fn(valueNode)
25-
}
26-
}
27-
})
28-
}
29-
}
12+
/**
13+
* @typedef {import('eslint-plugin-jsonc').AST.JSONLiteral} JSONLiteral
14+
*/
3015

3116
function findHTMLNode (node) {
3217
return node.childNodes.find(child => {
@@ -39,28 +24,24 @@ function findHTMLNode (node) {
3924
function create (context) {
4025
const filename = context.getFilename()
4126

42-
function verifyJson (jsonString, jsonFilename, offsetLoc = { line: 1, column: 1 }) {
43-
const ast = generateJsonAst(context, jsonString, jsonFilename)
44-
if (!ast) { return }
45-
46-
traverseNode(ast, messageNode => {
47-
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true })
48-
const foundNode = findHTMLNode(htmlNode)
49-
if (!foundNode) { return }
50-
const loc = {
51-
line: messageNode.loc.start.line,
52-
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset
53-
}
54-
if (loc.line === 1) {
55-
loc.line += offsetLoc.line - 1
56-
loc.column += offsetLoc.column - 1
57-
} else {
58-
loc.line += offsetLoc.line - 1
59-
}
60-
context.report({
61-
message: `used HTML localization message`,
62-
loc
63-
})
27+
/**
28+
* @param {JSONLiteral} node
29+
*/
30+
function verifyJSONLiteral (node) {
31+
const parent = node.parent
32+
if (parent.type === 'JSONProperty' && parent.key === node) {
33+
return
34+
}
35+
const htmlNode = parse5.parseFragment(`${node.value}`, { sourceCodeLocationInfo: true })
36+
const foundNode = findHTMLNode(htmlNode)
37+
if (!foundNode) { return }
38+
const loc = {
39+
line: node.loc.start.line,
40+
column: node.loc.start.column + 1/* quote */ + foundNode.sourceCodeLocation.startOffset
41+
}
42+
context.report({
43+
message: `used HTML localization message`,
44+
loc
6445
})
6546
}
6647

@@ -70,27 +51,40 @@ function create (context) {
7051
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
7152
/** @type {VElement[]} */
7253
const i18nBlocks = documentFragment && documentFragment.children.filter(node => node.type === 'VElement' && node.name === 'i18n') || []
54+
if (!i18nBlocks.length) {
55+
return
56+
}
57+
const localeMessages = getLocaleMessages(context)
7358

7459
for (const block of i18nBlocks) {
75-
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src') || !block.endTag) {
60+
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src')) {
61+
continue
62+
}
63+
64+
const targetLocaleMessage = localeMessages.findBlockLocaleMessage(block)
65+
if (!targetLocaleMessage) {
7666
continue
7767
}
78-
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
79-
const tokens = tokenStore.getTokensBetween(block.startTag, block.endTag)
80-
const jsonString = tokens.map(t => t.value).join('')
81-
if (jsonString.trim()) {
82-
verifyJson(jsonString, filename, block.startTag.loc.start)
68+
const ast = targetLocaleMessage.getJsonAST()
69+
if (!ast) {
70+
continue
8371
}
72+
73+
traverseNodes(ast, {
74+
enterNode (node) {
75+
if (node.type !== 'JSONLiteral') {
76+
return
77+
}
78+
verifyJSONLiteral(node)
79+
},
80+
leaveNode () {}
81+
})
8482
}
8583
}
8684
}
87-
} else if (extname(filename) === '.json' && getLocaleMessages(context).findExistLocaleMessage(filename)) {
85+
} else if (context.parserServices.isJSON && getLocaleMessages(context).findExistLocaleMessage(filename)) {
8886
return {
89-
Program (node) {
90-
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
91-
if (!jsonString || !jsonFilename) { return }
92-
verifyJson(jsonString, jsonFilename)
93-
}
87+
JSONLiteral: verifyJSONLiteral
9488
}
9589
} else {
9690
debug(`ignore ${filename} in no-html-messages`)

0 commit comments

Comments
 (0)