Skip to content

Commit

Permalink
feat(no-unlocalized-strings): add patterns for ignore functions
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko committed Nov 4, 2024
1 parent a407d3d commit 20cb248
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 46 deletions.
19 changes: 18 additions & 1 deletion docs/rules/no-unlocalized-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,24 @@ This option also supports member expressions. Example for `{ "ignoreFunction": [
console.log('Log this message')
```

> **Note:** Only single-level patterns are supported. For instance, `foo.bar.baz` will not be matched.
You can use patterns (processed by [micromatch](https://www.npmjs.com/package/micromatch)) to match function calls.

```js
/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreFunction": ["console.*"]}]*/
console.log('Log this message')
```

```js
/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreFunction": ["*.headers.set"]}]*/
context.headers.set('Authorization', `Bearer ${token}`)
```

Dynamic segments are replaced with `$`, you can target them as

```js
/*eslint lingui/no-unlocalized-strings: ["error", {"ignoreFunction": ["foo.$.set"]}]*/
foo[getName()].set('Hello')
```

### `ignoreAttribute`

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@typescript-eslint/utils": "^8.0.0"
"@typescript-eslint/utils": "^8.0.0",
"micromatch": "^4.0.0"
},
"peerDependencies": {
"eslint": "^8.37.0 || ^9.0.0"
},
"devDependencies": {
"@types/eslint": "^8.40.2",
"@types/jest": "^29.5.13",
"@types/micromatch": "^4.0.9",
"@types/node": "^20.3.3",
"@typescript-eslint/parser": "^8.0.0",
"@typescript-eslint/rule-tester": "^8.0.0",
Expand Down
29 changes: 29 additions & 0 deletions src/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { parse } from '@typescript-eslint/parser'
import { TSESTree } from '@typescript-eslint/utils'
import { buildCalleePath } from './helpers'

describe('buildCalleePath', () => {
function buildCallExp(code: string) {
const t = parse(code)

return (t.body[0] as TSESTree.ExpressionStatement).expression as TSESTree.CallExpression
}

it('Should build callee path', () => {
const exp = buildCallExp('one.two.three.four()')

expect(buildCalleePath(exp.callee)).toBe('one.two.three.four')
})

it('Should build with dynamic element', () => {
const exp = buildCallExp('one.two.three[getProp()]()')

expect(buildCalleePath(exp.callee)).toBe('one.two.three.$')
})

it('Should build with dynamic first element', () => {
const exp = buildCallExp('getData().two.three.four()')

expect(buildCalleePath(exp.callee)).toBe('$.two.three.four')
})
})
23 changes: 23 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,26 @@ export function isMemberExpression(
export function isJSXAttribute(node: TSESTree.Node | undefined): node is TSESTree.JSXAttribute {
return (node as TSESTree.Node)?.type === TSESTree.AST_NODE_TYPES.JSXAttribute
}

export function buildCalleePath(node: TSESTree.Expression) {
let current = node

const path: string[] = []

const push = (exp: TSESTree.Node) => {
if (isIdentifier(exp)) {
path.push(exp.name)
} else {
path.push('$')
}
}

while (isMemberExpression(current)) {
push(current.property)
current = current.object
}

push(current)

return path.reverse().join('.')
}
72 changes: 30 additions & 42 deletions src/rules/no-unlocalized-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TSESTree,
} from '@typescript-eslint/utils'
import {
buildCalleePath,
getIdentifierName,
getNearestAncestor,
getText,
Expand All @@ -18,6 +19,7 @@ import {
UpperCaseRegexp,
} from '../helpers'
import { createRule } from '../create-rule'
import * as micromatch from 'micromatch'

type MatcherDef = string | { regex: { pattern: string; flags?: string } }

Expand Down Expand Up @@ -159,7 +161,31 @@ export const rule = createRule<Option[], string>({
...((option && option.ignore) || []),
].map((item) => new RegExp(item))

const calleeWhitelists = generateCalleeWhitelists(option)
const calleeWhitelists = [
// popular callee
'*.addEventListener',
'*.removeEventListener',
'*.postMessage',
'*.getElementById',
'*.dispatch',
'*.commit',
'*.includes',
'*.indexOf',
'*.endsWith',
'*.startsWith',
'require',

// lingui callee
'i18n._',
't',
'plural',
'select',
...(option?.ignoreFunction || []),
].map((pattern) => micromatch.matcher(pattern))

const isCalleeWhitelisted = (callee: string) =>
calleeWhitelists.some((matcher) => matcher(callee))

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
Expand All @@ -172,16 +198,8 @@ export const rule = createRule<Option[], string>({
}: TSESTree.CallExpression | TSESTree.NewExpression): boolean {
switch (callee.type) {
case TSESTree.AST_NODE_TYPES.MemberExpression: {
if (isIdentifier(callee.property) && isIdentifier(callee.object)) {
if (calleeWhitelists.simple.includes(callee.property.name)) {
return true
}

const calleeName = `${callee.object.name}.${callee.property.name}`

if (calleeWhitelists.complex.includes(calleeName)) {
return true
}
if (isCalleeWhitelisted(buildCalleePath(callee))) {
return true
}

// use power of TS compiler to exclude call on specific types, such Map.get, Set.get and so on
Expand All @@ -202,7 +220,7 @@ export const rule = createRule<Option[], string>({
return false
}
case TSESTree.AST_NODE_TYPES.Identifier: {
return calleeWhitelists.simple.includes(callee.name)
return isCalleeWhitelisted(callee.name)
}
case TSESTree.AST_NODE_TYPES.CallExpression: {
return (
Expand Down Expand Up @@ -570,33 +588,3 @@ export const rule = createRule<Option[], string>({
return wrapVisitor<TSESTree.Literal>(visitor)
},
})

const popularCallee = [
'addEventListener',
'removeEventListener',
'postMessage',
'getElementById',
'dispatch',
'commit',
'includes',
'indexOf',
'endsWith',
'startsWith',
'require',
]
function generateCalleeWhitelists(option: Option) {
const result = {
simple: ['t', 'plural', 'select', ...popularCallee],
complex: ['i18n._'],
}

;(option?.ignoreFunction || []).forEach((item) => {
if (item.includes('.')) {
result.complex.push(item)
} else {
result.simple.push(item)
}
})

return result
}
22 changes: 21 additions & 1 deletion tests/src/rules/no-unlocalized-strings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ ruleTester.run<string, Option[]>(name, rule, {
code: 'custom.wrapper()({message: "Hello!"})',
options: [{ ignoreFunction: ['custom.wrapper'] }],
},
{
name: 'Should ignore calls using complex object.method expression',
code: 'console.log("Hello")',
options: [{ ignoreFunction: ['console.log'] }],
},
{
name: 'Should ignore method calls using pattern',
code: 'console.log("Hello"); console.error("Hello");',
options: [{ ignoreFunction: ['console.*'] }],
},
{
name: 'Should ignore methods multilevel',
code: 'context.headers.set("Hello"); level.context.headers.set("Hello");',
options: [{ ignoreFunction: ['*.headers.set'] }],
},
{
name: 'Should ignore methods with dynamic segment ',
code: 'getData().two.three.four("Hello")',
options: [{ ignoreFunction: ['*.three.four'] }],
},
{ code: 'name === `Hello brat` || name === `Nice have`' },
{ code: 'switch(a){ case `a`: break; default: break;}' },
{ code: 'a.indexOf(`ios`)' },
Expand Down Expand Up @@ -69,7 +89,7 @@ ruleTester.run<string, Option[]>(name, rule, {
{
code: 'document.removeEventListener("click", (event) => { event.preventDefault() })',
},
{ code: 'window.postMessage("message", "*")' },
{ code: 'window.postMessage("message", "Ola!")' },
{ code: 'document.getElementById("some-id")' },
{ code: 'require("hello");' },
{ code: 'const a = require(["hello"]);' },
Expand Down
30 changes: 29 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,13 @@ __metadata:
languageName: node
linkType: hard

"@types/braces@npm:*":
version: 3.0.4
resolution: "@types/braces@npm:3.0.4"
checksum: 10c0/05220f330814364318a1c2f403046d717690cabf3adc8417357b0b12713f92768cf9df76e0e25212b5a2727c8c56765ff19a284c7ece39e0985d0d6fadb6c4aa
languageName: node
linkType: hard

"@types/eslint@npm:^8.40.2":
version: 8.40.2
resolution: "@types/eslint@npm:8.40.2"
Expand Down Expand Up @@ -1091,6 +1098,15 @@ __metadata:
languageName: node
linkType: hard

"@types/micromatch@npm:^4.0.9":
version: 4.0.9
resolution: "@types/micromatch@npm:4.0.9"
dependencies:
"@types/braces": "npm:*"
checksum: 10c0/b13d7594b4320f20729f20156c51e957d79deb15083f98a736689cd0d3e4ba83b5d125959f6edf65270a6b6db90db9cebef8168d88e1c4eedc9a18aecc0234a3
languageName: node
linkType: hard

"@types/node@npm:*, @types/node@npm:^20.3.3":
version: 20.4.0
resolution: "@types/node@npm:20.4.0"
Expand Down Expand Up @@ -1484,7 +1500,7 @@ __metadata:
languageName: node
linkType: hard

"braces@npm:^3.0.2":
"braces@npm:^3.0.2, braces@npm:^3.0.3":
version: 3.0.3
resolution: "braces@npm:3.0.3"
dependencies:
Expand Down Expand Up @@ -1939,6 +1955,7 @@ __metadata:
dependencies:
"@types/eslint": "npm:^8.40.2"
"@types/jest": "npm:^29.5.13"
"@types/micromatch": "npm:^4.0.9"
"@types/node": "npm:^20.3.3"
"@typescript-eslint/parser": "npm:^8.0.0"
"@typescript-eslint/rule-tester": "npm:^8.0.0"
Expand All @@ -1948,6 +1965,7 @@ __metadata:
husky: "npm:^8.0.3"
jest: "npm:^29.5.0"
lint-staged: "npm:^14.0.0"
micromatch: "npm:^4.0.0"
prettier: "npm:3.3.3"
ts-jest: "npm:^29.1.1"
ts-node: "npm:^10.9.1"
Expand Down Expand Up @@ -3475,6 +3493,16 @@ __metadata:
languageName: node
linkType: hard

"micromatch@npm:^4.0.0":
version: 4.0.8
resolution: "micromatch@npm:4.0.8"
dependencies:
braces: "npm:^3.0.3"
picomatch: "npm:^2.3.1"
checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8
languageName: node
linkType: hard

"mimic-fn@npm:^2.1.0":
version: 2.1.0
resolution: "mimic-fn@npm:2.1.0"
Expand Down

0 comments on commit 20cb248

Please sign in to comment.