Repository: edvardchen/eslint-plugin-i18next (js)

Issue:
Allow regex patterns in ignore lists
Add capability to add regex patterns along with strings in ignore lists.

For example, if I want to ignore all functions starting with "foo" (e.g. "foo()", "foo1()", "fooTwo()") and a function "bar()", I would add:

```js
{ ignoreCallee: [/foo.*/, 'bar'] }

```

This would allow non-exhaustive checks, that is, to ignore a list of items where elements are not known in advance (or tedious to enumerate).

Related to https://github.com/edvardchen/eslint-plugin-i18next/issues/18 (the code will probably be shared)


Pull request description:
feat: allow regex in ignore and ignoreCallee
allow user to ignore strings or callee by regular expression

BREAKING CHANGE: all patterns in ignoreCallee would be treated as regular expression

fix #19

Patch:
diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js
index 0218efc..351a87d 100644
--- a/lib/rules/no-literal-string.js
+++ b/lib/rules/no-literal-string.js
@@ -23,10 +23,8 @@ module.exports = {
         type: 'object',
         properties: {
           ignore: {
-            type: 'array',
-            items: {
-              type: 'string'
-            }
+            type: 'array'
+            // string or regexp
           },
           ignoreAttribute: {
             type: 'array',
@@ -35,10 +33,8 @@ module.exports = {
             }
           },
           ignoreCallee: {
-            type: 'array',
-            items: {
-              type: 'string'
-            }
+            type: 'array'
+            // string or regexp
           },
           ignoreProperty: {
             type: 'array',
@@ -66,7 +62,6 @@ module.exports = {
       ...((option && option.ignore) || [])
     ].map(item => new RegExp(item));
 
-    const calleeWhitelists = generateCalleeWhitelists(option);
     const message = 'disallow literal string';
     //----------------------------------------------------------------------
     // Helpers
@@ -75,21 +70,47 @@ module.exports = {
       return whitelists.some(item => item.test(str));
     }
 
+    const popularCallee = [
+      /^i18n(ext)?$/,
+      't',
+      'require',
+      'addEventListener',
+      'removeEventListener',
+      'postMessage',
+      'getElementById',
+      //
+      // ─── VUEX CALLEE ────────────────────────────────────────────────────────────────
+      //
+      'dispatch',
+      'commit',
+      // ────────────────────────────────────────────────────────────────────────────────
+
+      'includes',
+      'indexOf',
+      'endsWith',
+      'startsWith'
+    ];
+
+    const validCalleeList = [
+      ...popularCallee,
+      ...((option && option.ignoreCallee) || [])
+    ].map(item => {
+      if (item instanceof RegExp) {
+        return item;
+      }
+      // allow dot ahead
+      return new RegExp(`(^|\\.)${item}${item.endsWith('$') ? '' : '$'}`);
+    });
+
     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)
-          return true;
-
-        calleeName = `${callee.object.name}.${callee.property.name}`;
-        return calleeWhitelists.complex.indexOf(calleeName) !== -1;
-      }
-
-      if (calleeName === 'require') return true;
+      const sourceText = context.getSourceCode().getText(callee);
 
-      return calleeWhitelists.simple.indexOf(calleeName) !== -1;
+      return validCalleeList.some(item => {
+        return item.test(sourceText);
+      });
     }
 
     const ignoredObjectProperties = (option && option.ignoreProperty) || [];
@@ -380,36 +401,3 @@ module.exports = {
     );
   }
 };
-
-const popularCallee = [
-  'addEventListener',
-  'removeEventListener',
-  'postMessage',
-  'getElementById',
-  //
-  // ─── VUEX CALLEE ────────────────────────────────────────────────────────────────
-  //
-  'dispatch',
-  'commit',
-  // ────────────────────────────────────────────────────────────────────────────────
-
-  'includes',
-  'indexOf',
-  'endsWith',
-  'startsWith'
-];
-function generateCalleeWhitelists(option) {
-  const ignoreCallee = (option && option.ignoreCallee) || [];
-  const result = {
-    simple: ['i18n', 'i18next', ...popularCallee],
-    complex: ['i18n.t', 'i18next.t']
-  };
-  ignoreCallee.forEach(item => {
-    if (item.indexOf('.') !== -1) {
-      result.complex.push(item);
-    } else {
-      result.simple.push(item);
-    }
-  });
-  return result;
-}


Test patch:
diff --git a/tests/lib/rules/no-literal-string.js b/tests/lib/rules/no-literal-string.js
index e96e03b..51b48a4 100644
--- a/tests/lib/rules/no-literal-string.js
+++ b/tests/lib/rules/no-literal-string.js
@@ -61,7 +61,15 @@ ruleTester.run('no-literal-string', rule, {
     { code: 'store.commit("hello");' },
     { code: 'i18n.t("hello");' },
     { code: 'const a = "absfoo";', options: [{ ignore: ['foo'] }] },
-    { code: 'const a = "fooabc";', options: [{ ignore: ['^foo'] }] },
+    { code: 'const a = "fooabc";', options: [{ ignore: [/^foo/] }] },
+    {
+      code: 'foo.bar("taa");',
+      options: [
+        {
+          ignoreCallee: [/foo.+/]
+        }
+      ]
+    },
     { code: 'const a = "FOO";' },
     { code: 'var A_B = "world";' },
     { code: 'var a = {["A_B"]: "hello world"};' },
@@ -110,6 +118,7 @@ ruleTester.run('no-literal-string', rule, {
   ],
 
   invalid: [
+    { code: 'i18nextXt("taa");', errors },
     { code: 'a + "b"', errors },
     {
       code: "switch(a){ case 'a': var a ='b'; break; default: break;}",