Skip to content

Commit

Permalink
refactor: use rule selectors to reduce code complexity
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Disable fix because key in the call i18next.t(key) ussally was not same as the plain text
  • Loading branch information
edvardchen committed Jun 20, 2019
1 parent 10016ba commit 28d73ff
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 62 deletions.
147 changes: 85 additions & 62 deletions lib/rules/no-literal-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ module.exports = {
category: 'Best Practices',
recommended: true
},
fixable: 'code', // or "code" or "whitespace"
schema: [
{
type: 'object',
Expand Down Expand Up @@ -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)
Expand All @@ -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 <div className="active" />
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 });
}
}
};
Expand All @@ -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
Expand Down
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
28 changes: 28 additions & 0 deletions tests/lib/rules/no-literal-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ ruleTester.run('no-literal-string', rule, {
{ code: 'var a = {A_B: "hello world"};' },
{ code: 'var a = {foo: "FOO"};' },
// JSX
{ code: '<div className="primary"></div>' },
{ code: '<div className={a ? "active": "inactive"}></div>' },
{ code: '<div>{i18next.t("foo")}</div>' }
],

Expand Down Expand Up @@ -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: '<div className="hello"></div>' }],
invalid: [
{
code: `(<button className={styles.btn}>loading</button>)`,
errors
}
]
});
// ────────────────────────────────────────────────────────────────────────────────

0 comments on commit 28d73ff

Please sign in to comment.