Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

eslint-plugin-i18next

Package Overview
Dependencies
Maintainers
1
Versions
51
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eslint-plugin-i18next - npm Package Compare versions

Comparing version 4.5.0 to 5.0.0-0

test.ts

9

CHANGELOG.md

@@ -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 @@

322

lib/rules/no-literal-string.js

@@ -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 },
],
});
// ────────────────────────────────────────────────────────────────────────────────
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc