From 952e5640c4c7465182c61b0a5c5981c083f3560c Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 11 Jul 2019 00:45:00 +0800 Subject: [PATCH 1/6] refactor: add prehandler for every selector of visitor --- lib/rules/no-literal-string.js | 25 +++++++++++++++++++++---- tests/lib/rules/no-literal-string.js | 1 + 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index bd5690b..8292f1d 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -95,6 +95,10 @@ module.exports = { return temp; } + function isString(node) { + return typeof node.value === 'string'; + } + const scriptVisitor = { // // ─── EXPORT AND IMPORT ─────────────────────────────────────────── @@ -162,7 +166,6 @@ module.exports = { }, '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); @@ -174,14 +177,11 @@ module.exports = { }, 'CallExpression Literal'(node) { - if (visited.includes(node)) return; const parent = getNearestAncestor(node, 'CallExpression'); if (isValidFunctionCall(parent)) visited.push(node); }, 'Literal:exit'(node) { - if (visited.includes(node)) return; - if (typeof node.value === 'string') { const trimed = node.value.trim(); if (!trimed) return; @@ -196,6 +196,23 @@ module.exports = { } } }; + + function wrapVisitor() { + Object.keys(scriptVisitor).forEach(key => { + const old = scriptVisitor[key]; + scriptVisitor[key] = node => { + // make sure node is string literal + if (!isString(node)) return; + + // visited and passed linting + if (visited.includes(node)) return; + old(node); + }; + }); + } + + wrapVisitor(); + return ( (parserServices.defineTemplateBodyVisitor && parserServices.defineTemplateBodyVisitor( diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index 365945e..d843eb6 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -35,6 +35,7 @@ ruleTester.run('no-literal-string', rule, { { code: 'require("hello");' }, { code: 'const a = require(["hello"]);' }, { code: 'const a = require(["hel" + "lo"]);' }, + { code: 'const a = 1;' }, { code: 'i18n("hello");' }, { code: 'dispatch("hello");' }, { code: 'store.dispatch("hello");' }, From 40c54b162a20d6a2502d56970487137843da6b1c Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Wed, 10 Jul 2019 21:07:38 +0800 Subject: [PATCH 2/6] feat: skip literal with LiteralType --- lib/rules/no-literal-string.js | 17 ++++++++ tests/lib/rules/no-literal-string.js | 25 +++++------ tests/lib/rules/tsconfig.json | 65 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 tests/lib/rules/tsconfig.json diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 8292f1d..5c2c1ad 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -5,6 +5,7 @@ 'use strict'; const { isUpperCase } = require('../helper'); +// const { TypeFlags, SyntaxKind } = require('typescript'); //------------------------------------------------------------------------------ // Rule Definition @@ -174,6 +175,22 @@ module.exports = { // dont care whether if this is computed or not if (isUpperCase(parent.key.name || parent.key.value)) visited.push(node); + + // + // TYPESCRIPT PART + // + + const { esTreeNodeToTSNodeMap, program } = parserServices; + if (program && esTreeNodeToTSNodeMap) { + const checker = program.getTypeChecker(); + const tsNode = esTreeNodeToTSNodeMap.get(node); + const typeObj = checker.getTypeAtLocation(tsNode.parent); + + if (typeObj.isStringLiteral()) { + visited.push(node); + } + // • • • • • + } }, 'CallExpression Literal'(node) { diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index d843eb6..31a1cc8 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -9,7 +9,8 @@ //------------------------------------------------------------------------------ var rule = require('../../../lib/rules/no-literal-string'), - RuleTester = require('eslint').RuleTester; + RuleTester = require('eslint').RuleTester, + path = require('path'); //------------------------------------------------------------------------------ // Tests @@ -100,27 +101,25 @@ const tsTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', - ecmaFeatures: { - jsx: true - } + project: path.resolve(__dirname, 'tsconfig.json') } }); tsTester.run('no-literal-string', rule, { valid: [ - { code: '
' }, - { - code: "var a: Element['nodeName']" - }, - { - code: "var a: Omit" - } + { code: '
', filename: 'a.tsx' }, + { code: "var a: Element['nodeName']" }, + { code: "var a: Omit" }, + { code: "type T = {name: 'b'} ; var a: T = {name: 'b'}" } ], invalid: [ { - code: `()`, + code: ``, + filename: 'a.tsx', errors - } + }, + + { code: "var a: {type: string} = {type: 'bb'}", errors } ] }); // ──────────────────────────────────────────────────────────────────────────────── diff --git a/tests/lib/rules/tsconfig.json b/tests/lib/rules/tsconfig.json new file mode 100644 index 0000000..74212d6 --- /dev/null +++ b/tests/lib/rules/tsconfig.json @@ -0,0 +1,65 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "preserveWatchOutput": true, + "allowUnreachableCode": false, + "resolveJsonModule": true, + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": [ + "node" + ] /* Type declaration files to be included in compilation. */, + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} From 2af8b08c690ea2a5466dfdb0b7f8693e060129ca Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 11 Jul 2019 10:39:35 +0800 Subject: [PATCH 3/6] chore: use weakset instead of array --- lib/rules/no-literal-string.js | 56 +++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index 5c2c1ad..a7c8c6c 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -83,7 +83,7 @@ module.exports = { //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- - const visited = []; + const visited = new WeakSet(); function getNearestAncestor(node, type) { let temp = node.parent; @@ -100,6 +100,11 @@ module.exports = { return typeof node.value === 'string'; } + const { esTreeNodeToTSNodeMap, program } = parserServices; + let typeChecker; + if (program && esTreeNodeToTSNodeMap) + typeChecker = program.getTypeChecker(); + const scriptVisitor = { // // ─── EXPORT AND IMPORT ─────────────────────────────────────────── @@ -107,17 +112,17 @@ module.exports = { 'ImportDeclaration Literal'(node) { // allow (import abc form 'abc') - visited.push(node); + visited.add(node); }, 'ExportAllDeclaration Literal'(node) { // allow export * from 'mod' - visited.push(node); + visited.add(node); }, 'ExportNamedDeclaration Literal'(node) { // allow export { named } from 'mod' - visited.push(node); + visited.add(node); }, // ───────────────────────────────────────────────────────────────── @@ -134,14 +139,14 @@ module.exports = { // allow
if (isValidAttrName(parent.name.name)) { - visited.push(node); + visited.add(node); } }, // @typescript-eslint/parser would parse string literal as JSXText node JSXText(node) { const trimed = node.value.trim(); - visited.push(node); + visited.add(node); if (!trimed || match(trimed)) { return; @@ -157,37 +162,34 @@ module.exports = { 'TSLiteralType Literal'(node) { // allow var a: Type['member']; - visited.push(node); + visited.add(node); }, // ───────────────────────────────────────────────────────────────── 'VariableDeclarator > Literal'(node) { // allow statements like const A_B = "test" - if (isUpperCase(node.parent.id.name)) visited.push(node); + if (isUpperCase(node.parent.id.name)) visited.add(node); }, 'Property > Literal'(node) { const { parent } = node; // if node is key of property, skip - if (parent.key === node) visited.push(node); + if (parent.key === node) visited.add(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); + if (isUpperCase(parent.key.name || parent.key.value)) visited.add(node); // // TYPESCRIPT PART // - const { esTreeNodeToTSNodeMap, program } = parserServices; - if (program && esTreeNodeToTSNodeMap) { - const checker = program.getTypeChecker(); + if (typeChecker) { const tsNode = esTreeNodeToTSNodeMap.get(node); - const typeObj = checker.getTypeAtLocation(tsNode.parent); + const typeObj = typeChecker.getTypeAtLocation(tsNode.parent); if (typeObj.isStringLiteral()) { - visited.push(node); + visited.add(node); } // • • • • • } @@ -195,22 +197,22 @@ module.exports = { 'CallExpression Literal'(node) { const parent = getNearestAncestor(node, 'CallExpression'); - if (isValidFunctionCall(parent)) visited.push(node); + if (isValidFunctionCall(parent)) visited.add(node); }, 'Literal:exit'(node) { - if (typeof node.value === 'string') { - const trimed = node.value.trim(); - if (!trimed) return; + // visited and passed linting + if (visited.has(node)) return; + const trimed = node.value.trim(); + if (!trimed) return; - const { parent } = node; + const { parent } = node; - // allow statements like const a = "FOO" - if (isUpperCase(trimed)) return; + // allow statements like const a = "FOO" + if (isUpperCase(trimed)) return; - if (match(trimed)) return; - context.report({ node, message }); - } + if (match(trimed)) return; + context.report({ node, message }); } }; @@ -221,8 +223,6 @@ module.exports = { // make sure node is string literal if (!isString(node)) return; - // visited and passed linting - if (visited.includes(node)) return; old(node); }; }); From 382ccab624fc18cac92058bc092f5c83bfe94bb1 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 11 Jul 2019 11:33:32 +0800 Subject: [PATCH 4/6] feat: more TS supports --- lib/rules/no-literal-string.js | 40 ++++++++++++++++++---------- tests/lib/rules/no-literal-string.js | 14 +++++++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js index a7c8c6c..e4d2c74 100644 --- a/lib/rules/no-literal-string.js +++ b/lib/rules/no-literal-string.js @@ -179,20 +179,6 @@ module.exports = { // 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.add(node); - - // - // TYPESCRIPT PART - // - - if (typeChecker) { - const tsNode = esTreeNodeToTSNodeMap.get(node); - const typeObj = typeChecker.getTypeAtLocation(tsNode.parent); - - if (typeObj.isStringLiteral()) { - visited.add(node); - } - // • • • • • - } }, 'CallExpression Literal'(node) { @@ -212,6 +198,32 @@ module.exports = { if (isUpperCase(trimed)) return; if (match(trimed)) return; + + // + // TYPESCRIPT + // + + if (typeChecker) { + const tsNode = esTreeNodeToTSNodeMap.get(node); + const typeObj = typeChecker.getTypeAtLocation(tsNode.parent); + + // var a: 'abc' = 'abc' + if (typeObj.isStringLiteral()) { + return; + } + + // var a: 'abc' | 'name' = 'abc' + if (typeObj.isUnion()) { + const found = typeObj.types.some(item => { + if (item.isStringLiteral() && item.value === node.value) { + return true; + } + }); + if (found) return; + } + } + // • • • • • + context.report({ node, message }); } }; diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js index 31a1cc8..17784b5 100644 --- a/tests/lib/rules/no-literal-string.js +++ b/tests/lib/rules/no-literal-string.js @@ -110,7 +110,11 @@ tsTester.run('no-literal-string', rule, { { code: '
', filename: 'a.tsx' }, { code: "var a: Element['nodeName']" }, { code: "var a: Omit" }, - { code: "type T = {name: 'b'} ; var a: T = {name: 'b'}" } + { code: `var a: 'abc' = 'abc'` }, + { code: `var a: 'abc' | 'name' | undefined= 'abc'` }, + { code: "type T = {name: 'b'} ; var a: T = {name: 'b'}" }, + { code: "function Button({ t= 'name' }: {t: 'name'}){} " }, + { code: "type T ={t?:'name'|'abc'};function Button({t='name'}:T){}" } ], invalid: [ { @@ -119,6 +123,14 @@ tsTester.run('no-literal-string', rule, { errors }, + { + code: "function Button({ t= 'name' }: {t: 'name' & 'abc'}){} ", + errors + }, + { + code: "function Button({ t= 'name' }: {t: 1 | 'abc'}){} ", + errors + }, { code: "var a: {type: string} = {type: 'bb'}", errors } ] }); From 479044fd3705b68c5fcd7e367c888be058d3dcd7 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 11 Jul 2019 12:05:52 +0800 Subject: [PATCH 5/6] docs: add more typescript examples restore HTML examples --- README.md | 79 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1490d83..4558373 100644 --- a/README.md +++ b/README.md @@ -43,23 +43,6 @@ in those projects which need to support [multi-language](https://www.i18next.com > 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: - -```js -// incorrect -const foo = 'foo'; - -// correct -const foo = i18next.t('foo'); -``` - -It is all right to use `UPPERCASE` string in javascript: - -```js -// correct -const foo = 'FOO'; -``` - ### Rule Details It will find out all literal strings and validate them. @@ -113,6 +96,34 @@ const bar = yourI18n('bar'); const bar = yourI18n.method('bar'); ``` +#### HTML Markup + +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): + +```HTML + + + + + +``` + #### Redux/Vuex This rule also works with those state managers like @@ -126,9 +137,36 @@ var bar = store.dispatch('bar'); var bar2 = store.commit('bar'); ``` -#### MISC +#### Typescript + +The following cases are **correct**: + +```typescript +// skip TSLiteralType +var a: Type['member']; +var a: Omit; + +// skip literal with LiteralType +var a: { t: 'button' } = { t: 'button' }; +var a: 'abc' | 'name' = 'abc'; +``` + +We require type information to work properly, so you need to add some options in your `.eslintrc`: -The following cases would be skip default: +```js + "parserOptions": { + // path of your tsconfig.json + "project": "./tsconfig.json" + } +``` + +See +[here](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage) +for more deteils. + +#### Import/Export + +The following cases are **correct**: ```typescript import mod from 'm'; @@ -137,9 +175,6 @@ require('mod'); export { named } from 'm'; export * from 'm'; - -var a: Type['member']; -var a: Omit; ``` ### Options From c175e8b13710e2924fb4e5cbc89f45e10db865c3 Mon Sep 17 00:00:00 2001 From: edvardchen <> Date: Thu, 11 Jul 2019 12:08:40 +0800 Subject: [PATCH 6/6] chore(release): 2.1.0 --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eec88e4..8208377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 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.1.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v2.0.0...v2.1.0) (2019-07-11) + + +### Features + +* more TS supports ([382ccab](https://github.com/edvardchen/eslint-plugin-i18next/commit/382ccab)) +* skip literal with LiteralType ([40c54b1](https://github.com/edvardchen/eslint-plugin-i18next/commit/40c54b1)) + + + ## [2.0.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v1.2.0...v2.0.0) (2019-07-10) diff --git a/package-lock.json b/package-lock.json index 46914ef..b7ae3d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-i18next", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5067de7..58e8f9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-i18next", - "version": "2.0.0", + "version": "2.1.0", "description": "ESLint plugin for i18n", "keywords": [ "eslint",