Add experimental AstServiceParser
This commit is contained in:
		| @@ -4,6 +4,7 @@ import { ParserInterface } from '../parsers/parser.interface'; | ||||
| import { PipeParser } from '../parsers/pipe.parser'; | ||||
| import { DirectiveParser } from '../parsers/directive.parser'; | ||||
| import { ServiceParser } from '../parsers/service.parser'; | ||||
| import { AstServiceParser } from '../parsers/ast-service.parser'; | ||||
| import { CompilerInterface } from '../compilers/compiler.interface'; | ||||
| import { JsonCompiler } from '../compilers/json.compiler'; | ||||
| import { NamespacedJsonCompiler } from '../compilers/namespaced-json.compiler'; | ||||
| @@ -19,18 +20,18 @@ const options = cli.parse({ | ||||
| 	format: ['f', 'Output format', ['json', 'namespaced-json', 'pot'], 'json'], | ||||
| 	replace: ['r', 'Replace the contents of output file if it exists (Merges by default)', 'boolean', false], | ||||
| 	sort: ['s', 'Sort translations in the output file in alphabetical order', 'boolean', false], | ||||
| 	clean: ['c', 'Remove obsolete strings when merging', 'boolean', false] | ||||
| 	clean: ['c', 'Remove obsolete strings when merging', 'boolean', false], | ||||
| 	experimental: ['e', 'Use experimental AST Service Parser', 'boolean', false] | ||||
| }); | ||||
|  | ||||
| const patterns: string[] = [ | ||||
| 	'/**/*.html', | ||||
| 	'/**/*.ts', | ||||
| 	'/**/*.js' | ||||
| 	'/**/*.ts' | ||||
| ]; | ||||
| const parsers: ParserInterface[] = [ | ||||
| 	new PipeParser(), | ||||
| 	new DirectiveParser(), | ||||
| 	new ServiceParser() | ||||
| 	options.experimental ? new AstServiceParser() : new ServiceParser() | ||||
| ]; | ||||
|  | ||||
| let compiler: CompilerInterface; | ||||
|   | ||||
							
								
								
									
										155
									
								
								src/parsers/ast-service.parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/parsers/ast-service.parser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| 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, ts.ScriptTarget.ES6, /*setParentNodes */ false); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Detect what the TranslateService instance property | ||||
| 	 * is called by inspecting constructor params | ||||
| 	 */ | ||||
| 	protected _getInstancePropertyName(): string { | ||||
| 		const constructorNode = this._findConstructorNode(); | ||||
| 		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 className: string = ( (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier).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); | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/utils/ast-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/utils/ast-utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import * as ts from 'typescript'; | ||||
|  | ||||
| export function printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0) { | ||||
| 	console.log( | ||||
| 		new Array(depth + 1).join('----'), | ||||
| 		`[${node.kind}]`, | ||||
| 		syntaxKindToName(node.kind), | ||||
| 		`[pos: ${node.pos}-${node.end}]`, | ||||
| 		':\t\t\t', | ||||
| 		node.getFullText(sourceFile).trim() | ||||
| 	); | ||||
|  | ||||
| 	depth++; | ||||
| 	node.getChildren(sourceFile).forEach(childNode => printAllChildren(sourceFile, childNode, depth)); | ||||
| } | ||||
|  | ||||
| export function syntaxKindToName(kind: ts.SyntaxKind) { | ||||
| 	return ts.SyntaxKind[kind]; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user