Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add extraction of enums from markers #5

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bartholomej/ngx-translate-extract",
"version": "8.0.2",
"name": "@gisaia/ngx-translate-extract",
"version": "8.1.0",
"description": "Extract strings from projects using ngx-translate",
"author": "Kim Biesbjerg <[email protected]>",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion src/parsers/marker.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class MarkerParser implements ParserInterface {
if (!firstArg) {
return;
}
const strings = getStringsFromExpression(firstArg);
const strings = getStringsFromExpression(firstArg, sourceFile);
collection = collection.addKeys(strings);
});
return collection;
Expand Down
2 changes: 1 addition & 1 deletion src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ServiceParser implements ParserInterface {
if (!firstArg) {
return;
}
const strings = getStringsFromExpression(firstArg);
const strings = getStringsFromExpression(firstArg, sourceFile);
collection = collection.addKeys(strings);
});
});
Expand Down
123 changes: 105 additions & 18 deletions src/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ import {
ConstructorDeclaration,
CallExpression,
Expression,
PropertyAccessExpression
PropertyAccessExpression,
EnumDeclaration,
StringLiteral,
SourceFile,
TypeReferenceNode,
EnumMember
} from 'typescript';

import pkg from 'typescript';
const { isIdentifier, isPropertyAccessExpression, isMemberName } = pkg;
import glob from 'glob';
import fs from 'fs';

const { SyntaxKind, isStringLiteralLike, isArrayLiteralExpression, isBinaryExpression, isConditionalExpression } = pkg;

export function getNamedImports(node: Node, moduleName: string): NamedImports[] {
Expand Down Expand Up @@ -90,7 +99,7 @@ export function findFunctionCallExpressions(node: Node, fnName: string | string[
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression:has(Identifier[name="${fnName}"]):not(:has(PropertyAccessExpression))`;
const query = `CallExpression:has(Identifier[name="${fnName}"])`;
const nodes = tsquery<CallExpression>(node, query);
return nodes;
}
Expand All @@ -104,43 +113,80 @@ export function findPropertyCallExpressions(node: Node, prop: string, fnName: st
return nodes;
}

export function getStringsFromExpression(expression: Expression): string[] {
export function findEnumDeclaration(node: SourceFile, enumIdentifier: string): EnumDeclaration | null {
const queryWhereEnum = `ImportDeclaration:has(ImportSpecifier[text=${enumIdentifier}]) StringLiteral`;
const [where] = tsquery<StringLiteral>(node, queryWhereEnum);
const query = `EnumDeclaration:has(Identifier[name=${enumIdentifier}])`;
if (!where) {
const [result] = tsquery<EnumDeclaration>(node, query);
return result;
} else {
const __dirname = process.cwd();
const filePath = [where.text, __dirname + '/' + where.text, __dirname + '/' + where.text + '.ts'];
for (var path of filePath) {
const enumFile = glob.sync(path).filter((filePath) => fs.statSync(filePath).isFile())[0];
if (enumFile) {
const contents: string = fs.readFileSync(enumFile, 'utf-8');
const [result] = tsquery<EnumDeclaration>(contents, query);
return result;
}
}
return null;
}
}

export function getStringsFromExpression(expression: Expression, sourceFile: SourceFile): string[] {
if (isStringLiteralLike(expression)) {
return [expression.text];
}

if (isArrayLiteralExpression(expression)) {
return expression.elements.reduce((result: string[], element: Expression) => {
const strings = getStringsFromExpression(element);
const strings = getStringsFromExpression(element, sourceFile);
return [...result, ...strings];
}, []);
}

if (isBinaryExpression(expression)) {
const [left] = getStringsFromExpression(expression.left);
const [right] = getStringsFromExpression(expression.right);
const left = getStringsFromExpression(expression.left, sourceFile);
const right = getStringsFromExpression(expression.right, sourceFile);

if (expression.operatorToken.kind === SyntaxKind.PlusToken) {
if (typeof left === 'string' && typeof right === 'string') {
return [left + right];
}
if (left.length + right.length === 0) {
return [];
}

if (expression.operatorToken.kind === SyntaxKind.BarBarToken) {
const result = [];
if (typeof left === 'string') {
result.push(left);
if (left.length === 0) {
return right;
}
if (typeof right === 'string') {
result.push(right);
if (right.length === 0) {
return left;
}
}

var results = [];
for (var leftValue of left) {
for (var rightValue of right) {
if (expression.operatorToken.kind === SyntaxKind.PlusToken) {
if (typeof leftValue === 'string' && typeof rightValue === 'string') {
results.push(leftValue + rightValue);
}
} else if (expression.operatorToken.kind === SyntaxKind.BarBarToken) {
if (typeof leftValue === 'string') {
results.push(leftValue);
}
if (typeof rightValue === 'string') {
results.push(rightValue);
}
}
}
return result;
}
return results;
}

if (isConditionalExpression(expression)) {
const [whenTrue] = getStringsFromExpression(expression.whenTrue);
const [whenFalse] = getStringsFromExpression(expression.whenFalse);
const [whenTrue] = getStringsFromExpression(expression.whenTrue, sourceFile);
const [whenFalse] = getStringsFromExpression(expression.whenFalse, sourceFile);

const result = [];
if (typeof whenTrue === 'string') {
Expand All @@ -151,5 +197,46 @@ export function getStringsFromExpression(expression: Expression): string[] {
}
return result;
}

if (isPropertyAccessExpression(expression)) {
var enumMemberName: string;
if (isIdentifier(expression.name) && expression.name.escapedText) {
enumMemberName = expression.name.escapedText.toString();
} else {
return [];
}

if (isIdentifier(expression.expression)) {
const enumObject = findEnumDeclaration(sourceFile, expression.expression.escapedText.toString());
if (!enumObject) {
return [];
}
for (var enumMember of enumObject.members) {
if (isIdentifier(enumMember.name) && enumMember.name.escapedText && enumMember.name.escapedText.toString() === enumMemberName) {
if (enumMember.initializer && isStringLiteralLike(enumMember.initializer)) {
return [enumMember.initializer.text];
}
}
}
return [];
}
}
if (isMemberName(expression)) {
const [result] = tsquery<TypeReferenceNode>(sourceFile, `Parameter:has(Identifier[name=${expression.text}]) TypeReference`);
if (!!result) {
if (isIdentifier(result.typeName)) {
const enumObject = findEnumDeclaration(sourceFile, result.typeName.escapedText.toString());
if (!enumObject) {
return [];
}
return enumObject.members.reduce((result: string[], member: EnumMember) => {
if (member.initializer && isStringLiteralLike(member.initializer)) {
return [...result, member.initializer.text];
}
return result;
}, []);
}
}
}
return [];
}
50 changes: 50 additions & 0 deletions tests/parsers/marker.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,54 @@ describe('MarkerParser', () => {
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['DYNAMIC_TRAD.val1', 'DYNAMIC_TRAD.val2']);
});

it('should extract the value of string enums', () => {
const contents = `
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { TEST_ENUM } from './tests/utils/enum';

export enum DYNAMIC_TRAD {
string = 'string',
number = 5
}

export class AppModule {
constructor() {
marker(DYNAMIC_TRAD.string);
marker(DYNAMIC_TRAD.number.toString());
marker("Extract a " + DYNAMIC_TRAD.string + " value");
marker(TEST_ENUM.test);
marker(TEST_ENUM.wrong);
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['string', 'Extract a string value', 'test']);
});

it('should extract all possible values when a variable is an enum member', () => {
const contents = `
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { TEST_ENUM } from './tests/utils/enum';

export enum DYNAMIC_TRAD {
string1 = 'string1',
string2 = 'string2',
number = 5
}

export class AppModule {

constructor() {
}

public testFunction(enumVariable: DYNAMIC_TRAD) {
marker(enumVariable)
marker(enumVariable + " test")
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['string1', 'string2', 'string1 test', 'string2 test']);
});
});
4 changes: 4 additions & 0 deletions tests/utils/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum TEST_ENUM {
test = 'test',
wrong = 5
}
Loading