From 33d8c26a28f71f74764d9e24dd1700125a2bd7d6 Mon Sep 17 00:00:00 2001 From: Kim Biesbjerg Date: Sun, 22 Mar 2020 13:33:40 +0100 Subject: [PATCH] extract strings when TranslateService is accessed directly via constructor parameter. Closes #50 and #106 --- src/parsers/service.parser.ts | 29 ++++++++++++++++++++------ src/utils/ast-helpers.ts | 31 +++++++++++++++++++++++++--- tests/parsers/service.parser.spec.ts | 12 +++++++++++ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index ba295a5..5b6c9f1 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -1,8 +1,9 @@ +import { ClassDeclaration, CallExpression } from 'typescript'; import { tsquery } from '@phenomnomnominal/tsquery'; import { ParserInterface } from './parser.interface'; import { TranslationCollection } from '../utils/translation.collection'; -import { findClassDeclarations, findClassPropertyByType, findMethodCallExpressions, getStringsFromExpression } from '../utils/ast-helpers'; +import { findClassDeclarations, findClassPropertyByType, findPropertyCallExpressions, findMethodCallExpressions, getStringsFromExpression, findMethodParameterByType, findConstructorDeclaration } from '../utils/ast-helpers'; const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService'; const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream']; @@ -19,12 +20,11 @@ export class ServiceParser implements ParserInterface { let collection: TranslationCollection = new TranslationCollection(); classDeclarations.forEach(classDeclaration => { - const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE); - if (!propName) { - return; - } + const callExpressions = [ + ...this.findConstructorParamCallExpressions(classDeclaration), + ...this.findPropertyCallExpressions(classDeclaration) + ]; - const callExpressions = findMethodCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES); callExpressions.forEach(callExpression => { const [firstArg] = callExpression.arguments; if (!firstArg) { @@ -36,4 +36,21 @@ export class ServiceParser implements ParserInterface { }); return collection; } + + protected findConstructorParamCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] { + const constructorDeclaration = findConstructorDeclaration(classDeclaration); + if (!constructorDeclaration) { + return []; + } + const paramName = findMethodParameterByType(constructorDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE); + return findMethodCallExpressions(constructorDeclaration, paramName, TRANSLATE_SERVICE_METHOD_NAMES); + } + + protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] { + const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE); + if (!propName) { + return []; + } + return findPropertyCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES); + } } diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index c58e1da..060b12b 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -1,15 +1,16 @@ import { tsquery } from '@phenomnomnominal/tsquery'; import { + SyntaxKind, Node, NamedImports, Identifier, ClassDeclaration, - CallExpression, + ConstructorDeclaration, isStringLiteralLike, isArrayLiteralExpression, + CallExpression, Expression, isBinaryExpression, - SyntaxKind, isConditionalExpression, PropertyAccessExpression } from 'typescript'; @@ -45,6 +46,30 @@ export function findClassPropertyByType(node: ClassDeclaration, type: string): s return findClassPropertyConstructorParameterByType(node, type) || findClassPropertyDeclarationByType(node, type); } +export function findConstructorDeclaration(node: ClassDeclaration): ConstructorDeclaration { + const query = `Constructor`; + const [result] = tsquery(node, query); + return result; +} + +export function findMethodParameterByType(node: Node, type: string): string | null { + const query = `Parameter:has(TypeReference > Identifier[name="${type}"]) > Identifier`; + const [result] = tsquery(node, query); + if (result) { + return result.text; + } + return 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)))`; + const nodes = tsquery(node, query).map(n => n.parent as CallExpression); + return nodes; +} + export function findClassPropertyConstructorParameterByType(node: ClassDeclaration, type: string): string | null { const query = `Constructor Parameter:has(TypeReference > Identifier[name="${type}"]):has(PublicKeyword,ProtectedKeyword,PrivateKeyword) > Identifier`; const [result] = tsquery(node, query); @@ -72,7 +97,7 @@ export function findFunctionCallExpressions(node: Node, fnName: string | string[ return nodes; } -export function findMethodCallExpressions(node: Node, prop: string, fnName: string | string[]): CallExpression[] { +export function findPropertyCallExpressions(node: Node, prop: string, fnName: string | string[]): CallExpression[] { if (Array.isArray(fnName)) { fnName = fnName.join('|'); } diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts index 2e2b085..7eba5ff 100644 --- a/tests/parsers/service.parser.spec.ts +++ b/tests/parsers/service.parser.spec.ts @@ -11,6 +11,18 @@ describe('ServiceParser', () => { parser = new ServiceParser(); }); + it('should extract strings when TranslateService is accessed directly via constructor parameter', () => { + const contents = ` + @Component({ }) + export class MyComponent { + public constructor(protected translateService: TranslateService) { + translateService.get('It works!'); + } + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['It works!']); + }); + it('should support extracting binary expressions', () => { const contents = ` @Component({ })