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 + } + ] +}); +// ────────────────────────────────────────────────────────────────────────────────