Skip to content

Commit

Permalink
feat: Add support for extraction of translation keys from function ex…
Browse files Browse the repository at this point in the history
…pressions (#46)

Closes #44
  • Loading branch information
pmpak authored May 21, 2024
1 parent 0771f62 commit afa2e69
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 13 deletions.
30 changes: 22 additions & 8 deletions src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
findMethodParameterByType,
findConstructorDeclaration,
getSuperClassName,
getImportPath
getImportPath,
findFunctionExpressions,
findVariableNameByInjectType
} from '../utils/ast-helpers.js';

const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
Expand All @@ -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;
}

Expand Down
25 changes: 20 additions & 5 deletions src/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export function findClassDeclarations(node: Node, name: string = null): ClassDec
return tsquery<ClassDeclaration>(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<Identifier>(node, query);
Expand Down Expand Up @@ -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<Identifier>(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<PropertyAccessExpression>(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[] {
Expand Down
63 changes: 63 additions & 0 deletions tests/parsers/service.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
});

0 comments on commit afa2e69

Please sign in to comment.