From 28d73ff5ae5734055bc77ac4cc8b24007d93aacd Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 20 Jun 2019 19:25:23 +0800 Subject: [PATCH 1/7] refactor: use rule selectors to reduce code complexity BREAKING CHANGE: Disable fix because key in the call i18next.t(key) ussally was not same as the plain text --- lib/rules/no-literal-string.js | 147 ++++++++++++++++----------- package-lock.json | 70 +++++++++++++ package.json | 2 + tests/lib/rules/no-literal-string.js | 28 +++++ 4 files changed, 185 insertions(+), 62 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 8703e8e..2dcccf2 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -17,7 +17,6 @@ module.exports = { category: 'Best Practices', recommended: true }, - fixable: 'code', // or "code" or "whitespace" schema: [ { type: 'object', @@ -61,6 +60,7 @@ module.exports = { function isValidFunctionCall({ callee }) { let calleeName = callee.name; + if (callee.type === 'Import') return true; if (callee.type === 'MemberExpression') { if (calleeWhitelists.simple.indexOf(callee.property.name) !== -1) @@ -74,61 +74,96 @@ module.exports = { return calleeWhitelists.simple.indexOf(calleeName) !== -1; } + const atts = ['className', 'style', 'styleName', 'src', 'type', 'id']; + function isValidAttrName(name) { + return atts.includes(name); + } + //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- + const visited = []; + function Literal(node) {} + + function getNearestAncestor(node, type) { + let temp = node.parent; + while (temp) { + if (temp.type === type) { + return temp; + } + temp = temp.parent; + } + return temp; + } + const scriptVisitor = { - Literal(node) { + 'JSXAttribute Literal'(node) { + const parent = getNearestAncestor(node, 'JSXAttribute'); + + // allow
+ if (isValidAttrName(parent.name.name)) { + visited.push(node); + } + }, + + 'ImportDeclaration Literal'(node) { + // allow (import abc form 'abc') + visited.push(node); + }, + + 'VariableDeclarator > Literal'(node) { + // allow statements like const A_B = "test" + if (isUpperCase(node.parent.id.name)) visited.push(node); + }, + + 'Property > Literal'(node) { + if (visited.includes(node)) return; + const { parent } = node; + // if node is key of property, skip + if (parent.key === node) visited.push(node); + + // name if key is Identifier; value if key is Literal + // dont care whether if this is computed or not + if (isUpperCase(parent.key.name || parent.key.value)) + visited.push(node); + }, + + 'CallExpression Literal'(node) { + if (visited.includes(node)) return; + const parent = getNearestAncestor(node, 'CallExpression'); + if (isValidFunctionCall(parent)) visited.push(node); + }, + + 'JSXElement > Literal'(node) { + scriptVisitor.JSXText(node); + }, + + // @typescript-eslint/parser would parse string literal as JSXText node + JSXText(node) { + const trimed = node.value.trim(); + visited.push(node); + + if (!trimed || match(trimed)) { + return; + } + + context.report({ node, message }); + }, + + 'Literal:exit'(node) { + if (visited.includes(node)) return; + if (typeof node.value === 'string') { const trimed = node.value.trim(); if (!trimed) return; const { parent } = node; - if (isUpperCase(trimed) && parent.type !== 'JSXElement') return; - - if (parent) { - switch (parent.type) { - case 'VariableDeclarator': { - if (isUpperCase(parent.id.name)) return; - break; - } - case 'Property': { - // if node is key of property, skip - if (parent.key === node) return; - // name if key is Identifier; value if key is Literal - // dont care whether if this is computed or not - if (isUpperCase(parent.key.name || parent.key.value)) return; - break; - } - case 'ImportDeclaration': // skip - return; - default: - let LOOK_UP_LIMIT = 3; - let temp = parent; - while (temp && LOOK_UP_LIMIT > 0) { - LOOK_UP_LIMIT--; - if (temp.type === 'CallExpression') { - // import(...) is valid - if (temp.callee.type === 'Import') return; - - if (isValidFunctionCall(temp)) return; - break; - } - temp = temp.parent; - } - break; - } - } + // allow statements like const a = "FOO" + if (isUpperCase(trimed)) return; if (match(trimed)) return; - context.report({ - node, - message, - fix(fixer) { - return fixer.replaceText(node, `i18next.t('${node.value}')`); - } - }); + context.report({ node, message }); } } }; @@ -137,25 +172,13 @@ module.exports = { parserServices.defineTemplateBodyVisitor( { VText(node) { - const trimed = node.value.trim(); - if (!trimed) return; - if (match(trimed)) return; - context.report({ - node, - message, - fix(fixer) { - return fixer.replaceText( - node, - node.value.replace( - /^(\s*)(.+?)(\s*)$/, // keep spaces - "$1{{i18next.t('$2')}}$3" - ) - ); - } - }); + scriptVisitor['JSXText'](node); + }, + 'VExpressionContainer CallExpression Literal'(node) { + scriptVisitor['CallExpression Literal'](node); }, - 'VExpressionContainer Literal'(node) { - scriptVisitor.Literal(node); + 'VExpressionContainer Literal:exit'(node) { + Literal(node); } }, scriptVisitor diff --git a/package-lock.json b/package-lock.json index 6532873..b08c6d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -442,6 +442,64 @@ "rimraf": "^2.5.2" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@typescript-eslint/experimental-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.10.2.tgz", + "integrity": "sha512-Hf5lYcrnTH5Oc67SRrQUA7KuHErMvCf5RlZsyxXPIT6AXa8fKTyfFO6vaEnUmlz48RpbxO4f0fY3QtWkuHZNjg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "1.10.2", + "eslint-scope": "^4.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.10.2.tgz", + "integrity": "sha512-xWDWPfZfV0ENU17ermIUVEVSseBBJxKfqBcRCMZ8nAjJbfA5R7NWMZmFFHYnars5MjK4fPjhu4gwQv526oZIPQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.10.2", + "@typescript-eslint/typescript-estree": "1.10.2", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.10.2.tgz", + "integrity": "sha512-Kutjz0i69qraOsWeI8ETqYJ07tRLvD9URmdrMoF10bG8y8ucLmPtSxROvVejWvlJUGl2et/plnMiKRDW+rhEhw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -1839,6 +1897,12 @@ "lodash._reinterpolate": "~3.0.0" } }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -2873,6 +2937,12 @@ "prelude-ls": "~1.1.2" } }, + "typescript": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "dev": true + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index 0fccead..a7a232c 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "devDependencies": { "@commitlint/cli": "^7.5.2", "@commitlint/config-conventional": "^7.5.0", + "@typescript-eslint/parser": "^1.10.2", "babel-eslint": "^10.0.1", "eslint": "^5.16.0", "husky": "^1.3.1", "mocha": "^6.1.4", + "typescript": "^3.5.2", "vue-eslint-parser": "^6.0.3" }, "engines": { diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index b250197..6787e54 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -47,6 +47,8 @@ ruleTester.run('no-literal-string', rule, { { code: 'var a = {A_B: "hello world"};' }, { code: 'var a = {foo: "FOO"};' }, // JSX + { code: '
' }, + { code: '
' }, { code: '
{i18next.t("foo")}
' } ], @@ -81,3 +83,29 @@ vueTester.run('no-literal-string', rule, { } ] }); +// ──────────────────────────────────────────────────────────────────────────────── + +// +// ─── TYPESCRIPT ───────────────────────────────────────────────────────────────── +// + +const tsTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + } +}); + +tsTester.run('no-literal-string', rule, { + valid: [{ code: '
' }], + invalid: [ + { + code: `()`, + errors + } + ] +}); +// ──────────────────────────────────────────────────────────────────────────────── From dd279c6dd4863a241c878d0e9d9238da90cd5b8b Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 15:10:56 +0800 Subject: [PATCH 2/7] fix: wrongly handle Literal node in VExpressionContainer --- lib/rules/no-literal-string.js | 3 +-- tests/lib/rules/no-literal-string.js | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 2dcccf2..3388def 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -83,7 +83,6 @@ module.exports = { // Public //---------------------------------------------------------------------- const visited = []; - function Literal(node) {} function getNearestAncestor(node, type) { let temp = node.parent; @@ -178,7 +177,7 @@ module.exports = { scriptVisitor['CallExpression Literal'](node); }, 'VExpressionContainer Literal:exit'(node) { - Literal(node); + scriptVisitor['Literal:exit'](node); } }, scriptVisitor diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index 6787e54..582ed1c 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -80,6 +80,10 @@ vueTester.run('no-literal-string', rule, { { code: '', errors + }, + { + code: '', + errors } ] }); From 1527eae7d04eefcdaffac22afa4a04995a764622 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 15:16:59 +0800 Subject: [PATCH 3/7] feat: dont check literal in export declaration --- lib/rules/no-literal-string.js | 10 ++++++++++ tests/lib/rules/no-literal-string.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 3388def..9210369 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -110,6 +110,16 @@ module.exports = { visited.push(node); }, + 'ExportAllDeclaration Literal'(node) { + // allow export * from 'mod' + visited.push(node); + }, + + 'ExportNamedDeclaration Literal'(node) { + // allow export { named } from 'mod' + visited.push(node); + }, + 'VariableDeclarator > Literal'(node) { // allow statements like const A_B = "test" if (isUpperCase(node.parent.id.name)) visited.push(node); diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index 582ed1c..c7bf778 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -30,6 +30,8 @@ ruleTester.run('no-literal-string', rule, { valid: [ { code: 'import("hello")' }, { code: 'import name from "hello";' }, + { code: 'export * from "hello_export_all";' }, + { code: 'export { a } from "hello_export";' }, { code: 'require("hello");' }, { code: 'const a = require(["hello"]);' }, { code: 'const a = require(["hel" + "lo"]);' }, From 57c5066cd70b3b34976acb84a31d4ee43ceab060 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 15:23:01 +0800 Subject: [PATCH 4/7] chore: reposition scriptVisitor selectors --- lib/rules/no-literal-string.js | 58 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 9210369..5e7391d 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -96,14 +96,9 @@ module.exports = { } const scriptVisitor = { - 'JSXAttribute Literal'(node) { - const parent = getNearestAncestor(node, 'JSXAttribute'); - - // allow
- if (isValidAttrName(parent.name.name)) { - visited.push(node); - } - }, + // + // ─── EXPORT AND IMPORT ─────────────────────────────────────────── + // 'ImportDeclaration Literal'(node) { // allow (import abc form 'abc') @@ -119,6 +114,37 @@ module.exports = { // allow export { named } from 'mod' visited.push(node); }, + // ───────────────────────────────────────────────────────────────── + + // + // ─── JSX ───────────────────────────────────────────────────────── + // + + 'JSXElement > Literal'(node) { + scriptVisitor.JSXText(node); + }, + + 'JSXAttribute Literal'(node) { + const parent = getNearestAncestor(node, 'JSXAttribute'); + + // allow
+ if (isValidAttrName(parent.name.name)) { + visited.push(node); + } + }, + + // @typescript-eslint/parser would parse string literal as JSXText node + JSXText(node) { + const trimed = node.value.trim(); + visited.push(node); + + if (!trimed || match(trimed)) { + return; + } + + context.report({ node, message }); + }, + // ───────────────────────────────────────────────────────────────── 'VariableDeclarator > Literal'(node) { // allow statements like const A_B = "test" @@ -143,22 +169,6 @@ module.exports = { if (isValidFunctionCall(parent)) visited.push(node); }, - 'JSXElement > Literal'(node) { - scriptVisitor.JSXText(node); - }, - - // @typescript-eslint/parser would parse string literal as JSXText node - JSXText(node) { - const trimed = node.value.trim(); - visited.push(node); - - if (!trimed || match(trimed)) { - return; - } - - context.report({ node, message }); - }, - 'Literal:exit'(node) { if (visited.includes(node)) return; From fd93861fb5728f3bb6a5354f7141584fbcf76d2d Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 15:27:16 +0800 Subject: [PATCH 5/7] feat: dont check TSLiteralType --- lib/rules/no-literal-string.js | 10 ++++++++++ tests/lib/rules/no-literal-string.js | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 5e7391d..bd5690b 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -146,6 +146,16 @@ module.exports = { }, // ───────────────────────────────────────────────────────────────── + // + // ─── TYPESCRIPT ────────────────────────────────────────────────── + // + + 'TSLiteralType Literal'(node) { + // allow var a: Type['member']; + visited.push(node); + }, + // ───────────────────────────────────────────────────────────────── + 'VariableDeclarator > Literal'(node) { // allow statements like const A_B = "test" if (isUpperCase(node.parent.id.name)) visited.push(node); diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index c7bf778..365945e 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -106,7 +106,15 @@ const tsTester = new RuleTester({ }); tsTester.run('no-literal-string', rule, { - valid: [{ code: '
' }], + valid: [ + { code: '
' }, + { + code: "var a: Element['nodeName']" + }, + { + code: "var a: Omit" + } + ], invalid: [ { code: `()`, From ee23dd77cb093a65003cba742acb88b6fa868023 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 15:43:07 +0800 Subject: [PATCH 6/7] docs: update --- README.md | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 89072df..1490d83 100644 --- a/README.md +++ b/README.md @@ -41,33 +41,7 @@ or This rule aims to avoid developers to display literal string to users in those projects which need to support [multi-language](https://www.i18next.com/). -The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. - -All literal strings in html template are typically mistakes. For JSX example: - -```HTML -
foo
-``` - -They should be translated by [i18next translation api](https://www.i18next.com/): - -```HTML -
{i18next.t('foo')}
-``` - -Same for [Vue template](https://vuejs.org/v2/guide/syntax.html): - -```vue - - - - - -``` +> Note: Disable auto-fix because key in the call `i18next.t(key)` ussally was not the same as the literal For plain javascript, literal strings that are not constant string (all characters are `UPPERCASE`) are disallowed: @@ -152,6 +126,22 @@ var bar = store.dispatch('bar'); var bar2 = store.commit('bar'); ``` +#### MISC + +The following cases would be skip default: + +```typescript +import mod from 'm'; +import('mod'); +require('mod'); + +export { named } from 'm'; +export * from 'm'; + +var a: Type['member']; +var a: Omit; +``` + ### Options #### ignore From 4d762e91e1155bb72917256b8ab73816f1b2cde5 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 21:13:32 +0800 Subject: [PATCH 7/7] chore(release): 2.0.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab99af..eec88e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.2.0...v2.0.0) (2019-07-10) + + +### Bug Fixes + +* wrongly handle Literal node in VExpressionContainer ([dd279c6](https://github.com/edvardchen/eslint-plugin-i18next/commit/dd279c6)) + + +### Features + +* dont check literal in export declaration ([1527eae](https://github.com/edvardchen/eslint-plugin-i18next/commit/1527eae)) +* dont check TSLiteralType ([fd93861](https://github.com/edvardchen/eslint-plugin-i18next/commit/fd93861)) + + +### refactor + +* use rule selectors to reduce code complexity ([28d73ff](https://github.com/edvardchen/eslint-plugin-i18next/commit/28d73ff)) + + +### BREAKING CHANGES + +* Disable fix because key in the call i18next.t(key) ussally was not same as the plain text + + + ## [1.2.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.1.3...v1.2.0) (2019-06-20) diff --git a/package-lock.json b/package-lock.json index b08c6d1..46914ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-i18next", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a7a232c..5067de7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-i18next", - "version": "1.2.0", + "version": "2.0.0", "description": "ESLint plugin for i18n", "keywords": [ "eslint",