eslint-plugin-i18next
Advanced tools
Comparing version 4.5.0 to 5.0.0-0
@@ -5,2 +5,11 @@ # Changelog | ||
## [5.0.0-0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.5.0...v5.0.0-0) (2020-08-28) | ||
### ⚠ BREAKING CHANGES | ||
* maybe break in some cases although all test cases passed | ||
* reuse all rules to template literal ([b5cdda6](https://github.com/edvardchen/eslint-plugin-i18next/commit/b5cdda6)), closes [#20](https://github.com/edvardchen/eslint-plugin-i18next/issues/20) | ||
## [4.5.0](https://github.com/edvardchen/eslint-plugin-i18next/compare/v4.4.0...v4.5.0) (2020-08-11) | ||
@@ -7,0 +16,0 @@ |
@@ -10,3 +10,3 @@ /** | ||
generateFullMatchRegExp, | ||
isAllowedDOMAttr | ||
isAllowedDOMAttr, | ||
} = require('../helper'); | ||
@@ -23,3 +23,3 @@ | ||
category: 'Best Practices', | ||
recommended: true | ||
recommended: true, | ||
}, | ||
@@ -31,3 +31,3 @@ schema: [ | ||
ignore: { | ||
type: 'array' | ||
type: 'array', | ||
// string or regexp | ||
@@ -38,7 +38,7 @@ }, | ||
items: { | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
}, | ||
ignoreCallee: { | ||
type: 'array' | ||
type: 'array', | ||
// string or regexp | ||
@@ -49,4 +49,4 @@ }, | ||
items: { | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
}, | ||
@@ -56,7 +56,7 @@ ignoreComponent: { | ||
items: { | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
}, | ||
markupOnly: { | ||
type: 'boolean' | ||
type: 'boolean', | ||
}, | ||
@@ -66,12 +66,12 @@ onlyAttribute: { | ||
items: { | ||
type: 'string' | ||
} | ||
type: 'string', | ||
}, | ||
}, | ||
validateTemplate: { | ||
type: 'boolean' | ||
} | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false | ||
} | ||
] | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
@@ -83,7 +83,18 @@ | ||
parserServices, | ||
options: [option] | ||
options: [ | ||
{ | ||
onlyAttribute = [], | ||
markupOnly: _markupOnly, | ||
validateTemplate, | ||
ignoreComponent = [], | ||
ignoreAttribute = [], | ||
ignoreProperty = [], | ||
ignoreCallee = [], | ||
ignore = [], | ||
} = {}, | ||
], | ||
} = context; | ||
const whitelists = [ | ||
/^[0-9!-/:-@[-`{-~]+$/, // ignore not-word string | ||
...((option && option.ignore) || []) | ||
...ignore, | ||
].map(item => new RegExp(item)); | ||
@@ -95,2 +106,12 @@ | ||
//---------------------------------------------------------------------- | ||
const indicatorStack = []; | ||
/** | ||
* detect if current "scope" is valid | ||
*/ | ||
function isValidScope() { | ||
return indicatorStack.some(item => item); | ||
} | ||
function match(str) { | ||
@@ -118,9 +139,8 @@ return whitelists.some(item => item.test(str)); | ||
'endsWith', | ||
'startsWith' | ||
'startsWith', | ||
]; | ||
const validCalleeList = [ | ||
...popularCallee, | ||
...((option && option.ignoreCallee) || []) | ||
].map(generateFullMatchRegExp); | ||
const validCalleeList = [...popularCallee, ...ignoreCallee].map( | ||
generateFullMatchRegExp | ||
); | ||
@@ -138,7 +158,4 @@ function isValidFunctionCall({ callee }) { | ||
const ignoredObjectProperties = (option && option.ignoreProperty) || []; | ||
const ignoredClassProperties = ['displayName']; | ||
const ignoredAttributes = (option && option.ignoreAttribute) || []; | ||
const userJSXAttrs = [ | ||
@@ -154,8 +171,8 @@ 'className', | ||
...ignoredAttributes | ||
...ignoreAttribute, | ||
]; | ||
function isValidAttrName(name) { | ||
if (option && option.onlyAttribute) { | ||
if (onlyAttribute.length) { | ||
// only validate those attributes in onlyAttribute option | ||
return !option.onlyAttribute.includes(name); | ||
return !onlyAttribute.includes(name); | ||
} | ||
@@ -166,26 +183,4 @@ return userJSXAttrs.includes(name); | ||
// Ignore the Trans component for react-i18next compatibility | ||
let ignoredComponents = ['Trans']; | ||
if (option && option.ignoreComponent) | ||
ignoredComponents = ignoredComponents.concat(option.ignoreComponent); | ||
const ignoredComponents = ['Trans', ...ignoreComponent]; | ||
function hasValidElementName(node) { | ||
let currentNode = node; | ||
while ( | ||
currentNode && | ||
currentNode.parent && | ||
currentNode.parent.openingElement && | ||
currentNode.parent.openingElement.name | ||
) { | ||
if ( | ||
ignoredComponents.includes( | ||
currentNode.parent.openingElement.name.name | ||
) | ||
) { | ||
return true; | ||
} | ||
currentNode = currentNode.parent; | ||
} | ||
return false; | ||
} | ||
//---------------------------------------------------------------------- | ||
@@ -216,15 +211,19 @@ // Public | ||
function validateLiteral(node) { | ||
// visited and passed linting | ||
if (visited.has(node)) return; | ||
const trimed = node.value.trim(); | ||
if (!trimed) return; | ||
function isValidLiteral(str) { | ||
const trimed = str.trim(); | ||
if (!trimed) return true; | ||
const { parent } = node; | ||
// allow statements like const a = "FOO" | ||
if (isUpperCase(trimed)) return; | ||
if (isUpperCase(trimed)) return true; | ||
if (match(trimed)) return; | ||
if (match(trimed)) return true; | ||
} | ||
function validateLiteralNode(node) { | ||
// make sure node is string literal | ||
if (!isString(node)) return; | ||
if (isValidLiteral(node.value)) { | ||
return; | ||
} | ||
// | ||
@@ -259,4 +258,8 @@ // TYPESCRIPT | ||
// onlyAttribute would turn on markOnly | ||
const markupOnly = option && (option.markupOnly || !!option.onlyAttribute); | ||
const markupOnly = _markupOnly || !!onlyAttribute.length; | ||
function endIndicator() { | ||
indicatorStack.pop(); | ||
} | ||
const scriptVisitor = { | ||
@@ -267,21 +270,25 @@ // | ||
'ImportExpression Literal'(node) { | ||
ImportExpression(node) { | ||
// allow (import('abc')) | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'ImportExpression:exit': endIndicator, | ||
'ImportDeclaration Literal'(node) { | ||
ImportDeclaration(node) { | ||
// allow (import abc form 'abc') | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'ImportDeclaration:exit': endIndicator, | ||
'ExportAllDeclaration Literal'(node) { | ||
ExportAllDeclaration(node) { | ||
// allow export * from 'mod' | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'ExportAllDeclaration:exit': endIndicator, | ||
'ExportNamedDeclaration > Literal'(node) { | ||
'ExportNamedDeclaration[source]'(node) { | ||
// allow export { named } from 'mod' | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'ExportNamedDeclaration[source]:exit': endIndicator, | ||
// ───────────────────────────────────────────────────────────────── | ||
@@ -293,2 +300,9 @@ | ||
JSXElement(node) { | ||
indicatorStack.push( | ||
ignoredComponents.includes(node.openingElement.name.name) | ||
); | ||
}, | ||
'JSXElement:exit': endIndicator, | ||
'JSXElement > Literal'(node) { | ||
@@ -298,24 +312,8 @@ scriptVisitor.JSXText(node); | ||
'JSXExpressionContainer > Literal:exit'(node) { | ||
if (markupOnly) { | ||
validateLiteral(node); | ||
} | ||
}, | ||
JSXAttribute(node) { | ||
const attrName = node.name.name; | ||
'JSXAttribute > Literal:exit'(node) { | ||
if (markupOnly) { | ||
const { | ||
name: { name: attrName } | ||
} = getNearestAncestor(node, 'JSXAttribute'); | ||
validateLiteral(node); | ||
} | ||
}, | ||
'JSXAttribute Literal'(node) { | ||
const parent = getNearestAncestor(node, 'JSXAttribute'); | ||
const attrName = parent.name.name; | ||
// allow <MyComponent className="active" /> | ||
if (isValidAttrName(attrName)) { | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
return; | ||
@@ -327,15 +325,25 @@ } | ||
if (isAllowedDOMAttr(tagName, attrName)) { | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
return; | ||
} | ||
indicatorStack.push(false); | ||
}, | ||
'JSXAttribute:exit': endIndicator, | ||
'JSXAttribute > Literal:exit'(node) { | ||
if (markupOnly) { | ||
if (isValidScope()) return; | ||
validateLiteralNode(node); | ||
} | ||
}, | ||
'JSXExpressionContainer > Literal:exit'(node) { | ||
scriptVisitor['JSXAttribute > Literal:exit'](node); | ||
}, | ||
// @typescript-eslint/parser would parse string literal as JSXText node | ||
JSXText(node) { | ||
if (isValidScope()) return; | ||
const trimed = node.value.trim(); | ||
visited.add(node); | ||
if (hasValidElementName(node)) { | ||
return; | ||
} | ||
if (!trimed || match(trimed)) { | ||
@@ -353,75 +361,68 @@ return; | ||
'TSLiteralType Literal'(node) { | ||
TSLiteralType(node) { | ||
// allow var a: Type['member']; | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'TSLiteralType:exit': endIndicator, | ||
// ───────────────────────────────────────────────────────────────── | ||
'ClassProperty > Literal'(node) { | ||
const { parent } = node; | ||
if (parent.key && ignoredClassProperties.includes(parent.key.name)) { | ||
visited.add(node); | ||
} | ||
ClassProperty(node) { | ||
indicatorStack.push( | ||
!!(node.key && ignoredClassProperties.includes(node.key.name)) | ||
); | ||
}, | ||
'ClassProperty:exit': endIndicator, | ||
'VariableDeclarator > Literal'(node) { | ||
VariableDeclarator(node) { | ||
// allow statements like const A_B = "test" | ||
if (isUpperCase(node.parent.id.name)) visited.add(node); | ||
indicatorStack.push(isUpperCase(node.id.name)); | ||
}, | ||
'VariableDeclarator:exit': endIndicator, | ||
'VariableDeclarator > ArrayExpression > Literal'(node) { | ||
// allow statements like const A_B = ["test"] | ||
const declarator = getNearestAncestor(node, 'VariableDeclarator'); | ||
if (isUpperCase(declarator.id.name)) visited.add(node); | ||
Property(node) { | ||
const result = | ||
// if node is key of property, skip | ||
node.key === node || | ||
ignoreProperty.includes(node.key.name) || | ||
// name if key is Identifier; value if key is Literal | ||
// dont care whether if this is computed or not | ||
isUpperCase(node.key.name || node.key.value); | ||
indicatorStack.push(result); | ||
}, | ||
'Property:exit': endIndicator, | ||
'Property > Literal'(node) { | ||
const { parent } = node; | ||
// if node is key of property, skip | ||
if (parent.key === node) visited.add(node); | ||
if (ignoredObjectProperties.includes(parent.key.name)) { | ||
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.add(node); | ||
}, | ||
'BinaryExpression > Literal'(node) { | ||
const { | ||
parent: { operator } | ||
} = node; | ||
BinaryExpression(node) { | ||
const { operator } = node; | ||
// allow name === 'Android' | ||
if (operator !== '+') { | ||
visited.add(node); | ||
} | ||
indicatorStack.push(operator !== '+'); | ||
}, | ||
'BinaryExpression:exit': endIndicator, | ||
'AssignmentPattern > Literal'(node) { | ||
AssignmentPattern(node) { | ||
// allow function bar(input = 'foo') {} | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'AssignmentPattern:exit': endIndicator, | ||
'CallExpression Literal'(node) { | ||
const parent = getNearestAncestor(node, 'CallExpression'); | ||
if (isValidFunctionCall(parent)) visited.add(node); | ||
CallExpression(node) { | ||
indicatorStack.push(isValidFunctionCall(node)); | ||
}, | ||
'CallExpression:exit': endIndicator, | ||
'SwitchCase > Literal'(node) { | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'SwitchCase > Literal:exit': endIndicator, | ||
'MemberExpression > Literal'(node) { | ||
MemberExpression(node) { | ||
// allow Enum['value'] | ||
visited.add(node); | ||
indicatorStack.push(true); | ||
}, | ||
'MemberExpression:exit': endIndicator, | ||
TemplateLiteral(node) { | ||
if (!option || !option.validateTemplate) { | ||
if (!validateTemplate) { | ||
return; | ||
} | ||
if (isValidScope()) return; | ||
const { quasis = [] } = node; | ||
@@ -435,3 +436,2 @@ quasis.some(({ value: { raw } }) => { | ||
}); | ||
// const trimed = node.value.trim(); | ||
}, | ||
@@ -443,23 +443,8 @@ | ||
} | ||
validateLiteral(node); | ||
} | ||
if (node.parent && node.parent.type === 'JSXElement') return; | ||
if (isValidScope()) return; | ||
validateLiteralNode(node); | ||
}, | ||
}; | ||
function wrapVisitor() { | ||
Object.keys(scriptVisitor).forEach(key => { | ||
const old = scriptVisitor[key]; | ||
scriptVisitor[key] = node => { | ||
// all visitors ends with Literal except TemplateLiteral | ||
if (key !== 'TemplateLiteral') { | ||
// make sure node is string literal | ||
if (!isString(node)) return; | ||
} | ||
old(node); | ||
}; | ||
}); | ||
} | ||
wrapVisitor(); | ||
return ( | ||
@@ -472,8 +457,11 @@ (parserServices.defineTemplateBodyVisitor && | ||
}, | ||
'VExpressionContainer CallExpression Literal'(node) { | ||
scriptVisitor['CallExpression Literal'](node); | ||
'VExpressionContainer CallExpression'(node) { | ||
scriptVisitor['CallExpression'](node); | ||
}, | ||
'VExpressionContainer CallExpression:exit'(node) { | ||
scriptVisitor['CallExpression:exit'](node); | ||
}, | ||
'VExpressionContainer Literal:exit'(node) { | ||
scriptVisitor['Literal:exit'](node); | ||
} | ||
}, | ||
}, | ||
@@ -484,3 +472,3 @@ scriptVisitor | ||
); | ||
} | ||
}, | ||
}; |
{ | ||
"name": "eslint-plugin-i18next", | ||
"version": "4.5.0", | ||
"version": "5.0.0-0", | ||
"description": "ESLint plugin for i18n", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -26,5 +26,5 @@ /** | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
} | ||
jsx: true, | ||
}, | ||
}, | ||
}); | ||
@@ -34,2 +34,3 @@ ruleTester.run('no-literal-string', rule, { | ||
{ code: 'import("hello")' }, | ||
{ code: 'import(`hello`)', options: [{ validateTemplate: true }] }, | ||
{ code: 'function bar(a="jianhua"){}' }, | ||
@@ -47,7 +48,7 @@ { code: "name === 'Android' || name === 'iOS'" }, | ||
code: | ||
'document.addEventListener("click", (event) => { event.preventDefault() })' | ||
'document.addEventListener("click", (event) => { event.preventDefault() })', | ||
}, | ||
{ | ||
code: | ||
'document.removeEventListener("click", (event) => { event.preventDefault() })' | ||
'document.removeEventListener("click", (event) => { event.preventDefault() })', | ||
}, | ||
@@ -73,7 +74,9 @@ { code: 'window.postMessage("message", "*")' }, | ||
{ | ||
ignoreCallee: [/foo.+/] | ||
} | ||
] | ||
ignoreCallee: [/foo.+/], | ||
}, | ||
], | ||
}, | ||
{ code: 'const a = "FOO";' }, | ||
{ code: 'const a = `FOO`;' }, | ||
{ code: 'var A_B = `world`;' }, | ||
{ code: 'var A_B = "world";' }, | ||
@@ -96,10 +99,10 @@ { code: 'var A_B = ["world"];' }, | ||
code: | ||
'<circle width="16px" height="16px" cx="10" cy="10" r="2" fill="red" />' | ||
'<circle width="16px" height="16px" cx="10" cy="10" r="2" fill="red" />', | ||
}, | ||
{ | ||
code: | ||
'<a href="https://google.com" target="_blank" rel="noreferrer noopener"></a>' | ||
'<a href="https://google.com" target="_blank" rel="noreferrer noopener"></a>', | ||
}, | ||
{ | ||
code: '<div id="some-id" tabIndex="0" aria-labelledby="label-id"></div>' | ||
code: '<div id="some-id" tabIndex="0" aria-labelledby="label-id"></div>', | ||
}, | ||
@@ -119,3 +122,3 @@ { code: '<div role="button"></div>' }, | ||
code: '<div>{[].map(item=>"abc")}</div>', | ||
options: [{ markupOnly: true }] | ||
options: [{ markupOnly: true }], | ||
}, | ||
@@ -126,3 +129,3 @@ { code: '<div>{"hello" + "world"}</div>', options: [{ markupOnly: true }] }, | ||
code: '<DIV foo="bar" />', | ||
options: [{ markupOnly: true, ignoreAttribute: ['foo'] }] | ||
options: [{ markupOnly: true, ignoreAttribute: ['foo'] }], | ||
}, | ||
@@ -133,12 +136,12 @@ // when onlyAttribute was configured, the markOnly would be treated as true | ||
{ | ||
code: 'var a = `hello world`' | ||
code: 'var a = `hello world`', | ||
}, | ||
{ | ||
code: 'var a = `12345`', | ||
options: [{ validateTemplate: true }] | ||
options: [{ validateTemplate: true }], | ||
}, | ||
{ | ||
code: 'var a = ``', | ||
options: [{ validateTemplate: true }] | ||
} | ||
options: [{ validateTemplate: true }], | ||
}, | ||
], | ||
@@ -150,3 +153,3 @@ | ||
options: [{ validateTemplate: true }], | ||
errors | ||
errors, | ||
}, | ||
@@ -156,3 +159,3 @@ { | ||
options: [{ validateTemplate: true }], | ||
errors | ||
errors, | ||
}, | ||
@@ -163,3 +166,3 @@ { code: 'i18nextXt("taa");', errors }, | ||
code: "switch(a){ case 'a': var a ='b'; break; default: break;}", | ||
errors | ||
errors, | ||
}, | ||
@@ -174,7 +177,7 @@ { code: 'export const a = "hello_string";', errors }, | ||
options: [{ ignoreProperty: ['bar'] }], | ||
errors | ||
errors, | ||
}, | ||
{ | ||
code: 'class Form extends Component { property = "Something" };', | ||
errors | ||
errors, | ||
}, | ||
@@ -188,3 +191,3 @@ // JSX | ||
options: [{ markupOnly: true }], | ||
errors | ||
errors, | ||
}, | ||
@@ -196,4 +199,4 @@ { code: '<div>フー</div>', errors }, | ||
{ code: '<img src="./image.png" alt="some-image" />', errors }, | ||
{ code: '<button aria-label="Close" type="button" />', errors } | ||
] | ||
{ code: '<button aria-label="Close" type="button" />', errors }, | ||
], | ||
}); | ||
@@ -208,4 +211,4 @@ | ||
parserOptions: { | ||
sourceType: 'module' | ||
} | ||
sourceType: 'module', | ||
}, | ||
}); | ||
@@ -217,10 +220,14 @@ | ||
{ | ||
code: '<template>{{ a("abc") }}</template>', | ||
errors, | ||
}, | ||
{ | ||
code: '<template>abc</template>', | ||
errors | ||
errors, | ||
}, | ||
{ | ||
code: '<template>{{"hello"}}</template>', | ||
errors | ||
} | ||
] | ||
errors, | ||
}, | ||
], | ||
}); | ||
@@ -237,4 +244,4 @@ // ──────────────────────────────────────────────────────────────────────────────── | ||
sourceType: 'module', | ||
project: path.resolve(__dirname, 'tsconfig.json') | ||
} | ||
project: path.resolve(__dirname, 'tsconfig.json'), | ||
}, | ||
}); | ||
@@ -252,3 +259,3 @@ | ||
{ code: "function Button({ t= 'name' }: {t: 'name'}){} " }, | ||
{ code: "type T ={t?:'name'|'abc'};function Button({t='name'}:T){}" } | ||
{ code: "type T ={t?:'name'|'abc'};function Button({t='name'}:T){}" }, | ||
], | ||
@@ -259,7 +266,7 @@ invalid: [ | ||
filename: 'a.tsx', | ||
errors | ||
errors, | ||
}, | ||
{ code: "var a: {type: string} = {type: 'bb'}", errors } | ||
] | ||
{ code: "var a: {type: string} = {type: 'bb'}", errors }, | ||
], | ||
}); | ||
// ──────────────────────────────────────────────────────────────────────────────── |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50264
13
945
2