Skip to content

Commit 547c27e

Browse files
committed
fix(rules): check strings inside exported functions
- isInImportExport incorrectly skipped all descendants of ExportNamedDeclaration - renamed to isImportExportSource, now only skips actual module source specifiers
1 parent 2c5dce1 commit 547c27e

File tree

2 files changed

+58
-13
lines changed

2 files changed

+58
-13
lines changed

src/rules/no-unlocalized-strings.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,37 @@ ruleTester.run("no-unlocalized-strings", noUnlocalizedStrings, {
348348
code: "<button>保存</button>",
349349
filename: "test.tsx",
350350
errors: [{ messageId: "unlocalizedString" }]
351+
},
352+
353+
// Strings inside exported functions (bug fix: should be flagged)
354+
{
355+
code: 'export function testAction() { const message = "Failed to check preferences"; return message; }',
356+
filename: "test.tsx",
357+
errors: [{ messageId: "unlocalizedString" }]
358+
},
359+
{
360+
code: 'export async function testAction() { const message = "Failed to check preferences"; return message; }',
361+
filename: "test.tsx",
362+
errors: [{ messageId: "unlocalizedString" }]
363+
},
364+
{
365+
code: `
366+
export async function checkPreferenceExistsAction(companyId: string) {
367+
try {
368+
return { success: true };
369+
} catch (error) {
370+
const message = error instanceof Error ? error.message : "Failed to check preferences";
371+
return { success: false, error: message };
372+
}
373+
}
374+
`,
375+
filename: "test.tsx",
376+
errors: [{ messageId: "unlocalizedString" }]
377+
},
378+
{
379+
code: 'export const testAction = () => { return "Something went wrong!" }',
380+
filename: "test.tsx",
381+
errors: [{ messageId: "unlocalizedString" }]
351382
}
352383
]
353384
})

src/rules/no-unlocalized-strings.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -569,18 +569,32 @@ function isInNonLinguiTaggedTemplate(node: TSESTree.Node): boolean {
569569
return false
570570
}
571571

572-
/** Checks if a string is in an import/export statement (module path). */
573-
function isInImportExport(node: TSESTree.Node): boolean {
574-
let current: TSESTree.Node | undefined = node.parent ?? undefined
572+
/**
573+
* Checks if a string is an import/export module source specifier.
574+
*
575+
* Only returns true for the module path in:
576+
* - import "module-path"
577+
* - import x from "module-path"
578+
* - export * from "module-path"
579+
* - export { x } from "module-path"
580+
*
581+
* Does NOT return true for strings inside exported declarations:
582+
* - export function foo() { return "Hello World" } ← "Hello World" should be checked
583+
*/
584+
function isImportExportSource(node: TSESTree.Node): boolean {
585+
const parent = node.parent
586+
if (parent === undefined) {
587+
return false
588+
}
575589

576-
while (current !== undefined) {
577-
switch (current.type) {
578-
case AST_NODE_TYPES.ImportDeclaration:
579-
case AST_NODE_TYPES.ExportAllDeclaration:
580-
case AST_NODE_TYPES.ExportNamedDeclaration:
581-
return true
582-
}
583-
current = current.parent ?? undefined
590+
// Check if this literal is the `source` property of an import/export
591+
switch (parent.type) {
592+
case AST_NODE_TYPES.ImportDeclaration:
593+
return parent.source === node
594+
case AST_NODE_TYPES.ExportAllDeclaration:
595+
return parent.source === node
596+
case AST_NODE_TYPES.ExportNamedDeclaration:
597+
return parent.source === node
584598
}
585599

586600
return false
@@ -804,8 +818,8 @@ export const noUnlocalizedStrings = createRule<[Options], MessageId>({
804818
return
805819
}
806820

807-
// Import/export path
808-
if (isInImportExport(node)) {
821+
// Import/export module source specifier
822+
if (isImportExportSource(node)) {
809823
return
810824
}
811825

0 commit comments

Comments
 (0)