- Added typings for packages where typings existed

- Remove regexp ServiceParser and make AstServiceParser the default. #23
- Replaced CLI parser to add support for multiple input/output paths (supports file expansion, glob patterns and multiple values/parameters) #7
This commit is contained in:
Kim Biesbjerg
2017-03-20 15:29:41 +01:00
parent c5d68cfcaa
commit 3facc0c287
10 changed files with 317 additions and 475 deletions

View File

@@ -1,163 +0,0 @@
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
import * as ts from 'typescript';
export class AstServiceParser implements ParserInterface {
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
this._instancePropertyName = this._getInstancePropertyName();
if (!this._instancePropertyName) {
return collection;
}
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor params
*/
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
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;
}
// Make sure className is of the correct type
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== this._serviceClassName) {
return false;
}
return true;
});
if (result) {
return (result.name as ts.Identifier).text;
}
}
/**
* Find first constructor
*/
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
// Only call expressions with arguments
.filter(callNode => callNode.arguments.length > 0)
// More filters
.filter(callNode => {
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== this._instancePropertyName) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
return false;
}
return true;
});
return callNodes;
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] {
if (node.kind === kind && onlyOne) {
return [node];
}
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
}

View File

@@ -1,59 +1,163 @@
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
import * as ts from 'typescript';
export class ServiceParser implements ParserInterface {
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) {
this._sourceFile = this._createSourceFile(path, contents);
this._instancePropertyName = this._getInstancePropertyName();
if (!this._instancePropertyName) {
return collection;
}
const methodRegExp: RegExp = /(?:get|instant)\s*\(\s*(\[?\s*(['"`])([^\1\r\n]*)\2\s*\]?)/;
const regExp: RegExp = new RegExp(`\\.${translateServiceVar}\\.${methodRegExp.source}`, 'g');
let matches: RegExpExecArray;
while (matches = regExp.exec(contents)) {
if (this._stringContainsArray(matches[1])) {
collection = collection.addKeys(this._stringToArray(matches[1]));
} else {
collection = collection.add(matches[3]);
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
}
});
return collection;
}
/**
* Extracts name of TranslateService variable for use in patterns
*/
protected _extractTranslateServiceVar(contents: string): string {
const matches = contents.match(/([a-z0-9_]+)\s*:\s*TranslateService/i);
if (matches === null) {
return '';
}
return matches[1];
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Checks if string contains an array
* Detect what the TranslateService instance property
* is called by inspecting constructor params
*/
protected _stringContainsArray(input: string): boolean {
return input.startsWith('[') && input.endsWith(']');
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
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;
}
// Make sure className is of the correct type
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== this._serviceClassName) {
return false;
}
return true;
});
if (result) {
return (result.name as ts.Identifier).text;
}
}
/**
* Converts string to array
* Find first constructor
*/
protected _stringToArray(input: string): string[] {
if (this._stringContainsArray(input)) {
return eval(input);
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
return [];
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
// Only call expressions with arguments
.filter(callNode => callNode.arguments.length > 0)
// More filters
.filter(callNode => {
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== this._instancePropertyName) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
return false;
}
return true;
});
return callNodes;
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] {
if (node.kind === kind && onlyOne) {
return [node];
}
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
}