- (chore) update packages

- (refactor) use tsquery for querying AST
- (feat) autodetect usage of marker function and remove --marker cli argument
- (bugfix) extract strings when TranslateService is declared directly as a class parameter. Closes https://github.com/biesbjerg/ngx-translate-extract/issues/83
- (bugfix) handle split strings: marker('hello ' + 'world') is now extracted as a single string: 'hello world'
This commit is contained in:
Kim Biesbjerg
2019-09-16 16:40:37 +02:00
parent 41bd679fcd
commit 4fe3c43624
13 changed files with 346 additions and 325 deletions

View File

@@ -1,48 +0,0 @@
import {
createSourceFile,
SourceFile,
CallExpression,
Node,
SyntaxKind,
StringLiteral,
NoSubstitutionTemplateLiteral
} from 'typescript';
export abstract class AbstractAstParser {
protected sourceFile: SourceFile;
protected createSourceFile(path: string, contents: string): SourceFile {
return createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Get strings from function call's first argument
*/
protected getStringLiterals(callNode: CallExpression): string[] {
if (!callNode.arguments.length) {
return[];
}
const firstArg = callNode.arguments[0];
return this.findNodes(firstArg, [
SyntaxKind.StringLiteral,
SyntaxKind.NoSubstitutionTemplateLiteral
])
.map((node: StringLiteral | NoSubstitutionTemplateLiteral) => node.text);
}
/**
* Find all child nodes of a kind
*/
protected findNodes(node: Node, kinds: SyntaxKind[]): Node[] {
const childrenNodes: Node[] = node.getChildren(this.sourceFile);
const initialValue: Node[] = kinds.includes(node.kind) ? [node] : [];
return childrenNodes.reduce((result: Node[], childNode: Node) => {
return result.concat(this.findNodes(childNode, kinds));
}, initialValue);
}
}

View File

@@ -1,60 +0,0 @@
import { Node, CallExpression, SyntaxKind, Identifier } from 'typescript';
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
export class FunctionParser extends AbstractAstParser implements ParserInterface {
protected functionIdentifier: string = 'marker';
public constructor(options?: any) {
super();
if (options && typeof options.identifier !== 'undefined') {
this.functionIdentifier = options.identifier;
}
}
public extract(template: string, path: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this.sourceFile = this.createSourceFile(path, template);
const callNodes = this.findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this.getStringLiterals(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
/**
* Find all calls to marker function
*/
protected findCallNodes(node?: Node): CallExpression[] {
if (!node) {
node = this.sourceFile;
}
let callNodes = this.findNodes(node, [SyntaxKind.CallExpression]) as CallExpression[];
callNodes = callNodes
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const identifier = (callNode.getChildAt(0) as Identifier).text;
if (identifier !== this.functionIdentifier) {
return false;
}
return true;
});
return callNodes;
}
}

View File

@@ -0,0 +1,34 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { getNamedImportAlias, findFunctionCallExpressions, getStringsFromExpression } from '../utils/ast-helpers';
const MARKER_PACKAGE_MODULE_NAME = '@biesbjerg/ngx-translate-extract-marker';
const MARKER_PACKAGE_IMPORT_NAME = 'marker';
export class MarkerParser implements ParserInterface {
public extract(contents: string, filePath: string): TranslationCollection {
const sourceFile = tsquery.ast(contents, filePath);
const markerFnName = getNamedImportAlias(sourceFile, MARKER_PACKAGE_MODULE_NAME, MARKER_PACKAGE_IMPORT_NAME);
if (!markerFnName) {
return;
}
let collection: TranslationCollection = new TranslationCollection();
const callNodes = findFunctionCallExpressions(sourceFile, markerFnName);
callNodes.forEach(callNode => {
const [firstArgNode] = callNode.arguments;
if (!firstArgNode) {
return;
}
const strings = getStringsFromExpression(firstArgNode);
collection = collection.addKeys(strings);
});
return collection;
}
}

View File

@@ -1,142 +1,41 @@
import {
SourceFile,
Node,
ConstructorDeclaration,
Identifier,
TypeReferenceNode,
ClassDeclaration,
SyntaxKind,
CallExpression,
PropertyAccessExpression,
isPropertyAccessExpression
} from 'typescript';
import { tsquery } from '@phenomnomnominal/tsquery';
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { findClasses, findClassPropertyByType, findMethodCallExpression, getStringsFromExpression } from '../utils/ast-helpers';
export class ServiceParser extends AbstractAstParser implements ParserInterface {
const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];
protected sourceFile: SourceFile;
export class ServiceParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection {
const sourceFile = tsquery.ast(source, filePath);
const classNodes = findClasses(sourceFile);
if (!classNodes) {
return;
}
public extract(template: string, path: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this.sourceFile = this.createSourceFile(path, template);
const classNodes = this.findClassNodes(this.sourceFile);
classNodes.forEach(classNode => {
const constructorNode = this.findConstructorNode(classNode);
if (!constructorNode) {
const propName: string = findClassPropertyByType(classNode, TRANSLATE_SERVICE_TYPE_REFERENCE);
if (!propName) {
return;
}
const propertyName: string = this.findTranslateServicePropertyName(constructorNode);
if (!propertyName) {
return;
}
const callNodes = this.findCallNodes(classNode, propertyName);
const callNodes = findMethodCallExpression(classNode, propName, TRANSLATE_SERVICE_METHOD_NAMES);
callNodes.forEach(callNode => {
const keys: string[] = this.getStringLiterals(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
const [firstArgNode] = callNode.arguments;
if (!firstArgNode) {
return;
}
const strings = getStringsFromExpression(firstArgNode);
collection = collection.addKeys(strings);
});
});
return collection;
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor arguments
*/
protected findTranslateServicePropertyName(constructorNode: ConstructorDeclaration): string {
if (!constructorNode) {
return null;
}
const result = constructorNode.parameters.find(parameter => {
// Skip if visibility modifier is not present (we want it set as an instance property)
if (!parameter.modifiers) {
return false;
}
// Parameter has no type
if (!parameter.type) {
return false;
}
// Make sure className is of the correct type
const parameterType: Identifier = (parameter.type as TypeReferenceNode).typeName as Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== 'TranslateService') {
return false;
}
return true;
});
if (result) {
return (result.name as Identifier).text;
}
}
/**
* Find class nodes
*/
protected findClassNodes(node: Node): ClassDeclaration[] {
return this.findNodes(node, [SyntaxKind.ClassDeclaration]) as ClassDeclaration[];
}
/**
* Find constructor
*/
protected findConstructorNode(node: ClassDeclaration): ConstructorDeclaration {
const constructorNodes = this.findNodes(node, [SyntaxKind.Constructor]) as ConstructorDeclaration[];
if (constructorNodes) {
return constructorNodes[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected findCallNodes(node: Node, propertyIdentifier: string): CallExpression[] {
let callNodes = this.findNodes(node, [SyntaxKind.CallExpression]) as CallExpression[];
callNodes = callNodes
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const propAccess = callNode.getChildAt(0).getChildAt(0) as PropertyAccessExpression;
if (!propAccess || !isPropertyAccessExpression(propAccess)) {
return false;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== propertyIdentifier) {
return false;
}
const methodAccess = callNode.getChildAt(0) as PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || (methodAccess.name.text !== 'get' && methodAccess.name.text !== 'instant' && methodAccess.name.text !== 'stream')) {
return false;
}
return true;
});
return callNodes;
}
}