diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index 9dcede9..d2cbb21 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -17,7 +17,9 @@ import { findMethodParameterByType, findConstructorDeclaration, getSuperClassName, - getImportPath + getImportPath, + findFunctionExpressions, + findVariableNameByInjectType } from '../utils/ast-helpers.js'; const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService'; @@ -28,29 +30,41 @@ export class ServiceParser implements ParserInterface { public extract(source: string, filePath: string): TranslationCollection | null { const sourceFile = tsquery.ast(source, filePath); - const classDeclarations = findClassDeclarations(sourceFile); - if (!classDeclarations) { + const functionDeclarations = findFunctionExpressions(sourceFile); + + if (!classDeclarations && !functionDeclarations) { return null; } let collection: TranslationCollection = new TranslationCollection(); + const translateServiceCallExpressions: CallExpression[] = []; + + functionDeclarations.forEach((fnDeclaration) => { + const translateServiceVariableName = findVariableNameByInjectType(fnDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE); + const callExpressions = findMethodCallExpressions(sourceFile, translateServiceVariableName, TRANSLATE_SERVICE_METHOD_NAMES); + translateServiceCallExpressions.push(...callExpressions); + }); + classDeclarations.forEach((classDeclaration) => { const callExpressions = [ ...this.findConstructorParamCallExpressions(classDeclaration), ...this.findPropertyCallExpressions(classDeclaration, sourceFile) ]; - callExpressions.forEach((callExpression) => { + translateServiceCallExpressions.push(...callExpressions); + }); + + translateServiceCallExpressions + .filter((callExpression) => !!callExpression.arguments?.[0]) + .forEach((callExpression) => { const [firstArg] = callExpression.arguments; - if (!firstArg) { - return; - } + const strings = getStringsFromExpression(firstArg); collection = collection.addKeys(strings, filePath); }); - }); + return collection; } diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index 62dab9d..6c9e6ee 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -42,6 +42,10 @@ export function findClassDeclarations(node: Node, name: string = null): ClassDec return tsquery(node, query); } +export function findFunctionExpressions(node: Node) { + return tsquery(node, 'VariableDeclaration > ArrowFunction, VariableDeclaration > FunctionExpression'); +} + export function getSuperClassName(node: Node): string | null { const query = 'ClassDeclaration > HeritageClause Identifier'; const [result] = tsquery(node, query); @@ -78,12 +82,23 @@ export function findMethodParameterByType(node: Node, type: string): string | nu return null; } +export function findVariableNameByInjectType(node: Node, type: string): string | null { + const query = `VariableDeclaration:has(Identifier[name="inject"]):has(CallExpression > Identifier[name="${type}"]) > Identifier`; + const [result] = tsquery(node, query); + + return result?.text ?? null; +} + export function findMethodCallExpressions(node: Node, propName: string, fnName: string | string[]): CallExpression[] { - if (Array.isArray(fnName)) { - fnName = fnName.join('|'); - } - const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${propName}"]):not(:has(ThisKeyword)))`; - return tsquery(node, query).map((n) => n.parent as CallExpression); + const functionNames = typeof fnName === 'string' ? [fnName] : fnName; + + const fnNameRegex = functionNames.join('|'); + + const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnNameRegex})$/]):has(PropertyAccessExpression:has(Identifier[name="${propName}"]):not(:has(ThisKeyword)))`; + + return tsquery(node, query) + .filter((n) => functionNames.includes(n.getLastToken().getText())) + .map((n) => n.parent as CallExpression); } export function findClassPropertiesConstructorParameterByType(node: ClassDeclaration, type: string): string[] { diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts index 8f6e235..66bcc86 100644 --- a/tests/parsers/service.parser.spec.ts +++ b/tests/parsers/service.parser.spec.ts @@ -547,4 +547,67 @@ describe('ServiceParser', () => { const keys = parser.extract(contents, componentFilename)?.keys(); expect(keys).to.deep.equal([]); }); + + describe('function expressions', () => { + it('should extract from arrow function expression', () => { + const contents = ` + export const errorInterceptor: HttpInterceptorFn = (req, next) => { + const translateService = inject(TranslateService); + const router = inject(Router); + + translateService.instant('translation.key'); + + const nestedFunction = () => { + translateService.instant('translation.key.from.nested.function'); + } + }; + `; + const keys = parser.extract(contents, componentFilename)?.keys(); + expect(keys).to.deep.equal(['translation.key', 'translation.key.from.nested.function']); + }); + + it('should extract from function expression', () => { + const contents = ` + export const errorInterceptor: HttpInterceptorFn = function(req, next) { + const translateService = inject(TranslateService); + const router = inject(Router); + + translateService.instant('translation.key') + + const nestedFunction = function() { + translateService.instant('translation.key.from.nested.function'); + } + }; + `; + const keys = parser.extract(contents, componentFilename)?.keys(); + expect(keys).to.deep.equal(['translation.key', 'translation.key.from.nested.function']); + }); + + it("should extract strings from TranslateService's get(), instant() and stream() method", () => { + const contents = ` + export const errorInterceptor: HttpInterceptorFn = (req, next) => { + const translate = inject(TranslateService); + + translate.get('get.translation.key') + translate.instant('instant.translation.key') + translate.stream('stream.translation.key') + }; + `; + const keys = parser.extract(contents, componentFilename)?.keys(); + expect(keys).to.deep.equal(['get.translation.key', 'instant.translation.key', 'stream.translation.key']); + }); + + it('should not extract chained function calls', () => { + const contents = ` + export const errorInterceptor: HttpInterceptorFn = (req, next) => { + const translate = inject(TranslateService); + + const strings = ['a', 'b', 'c']; + strings.map(string => translate.instant(string)).join(', '); + }; + `; + const keys = parser.extract(contents, componentFilename)?.keys(); + expect(keys).to.deep.equal([]); + }); + }); });