(refactor) simplify extraction of string literals
This commit is contained in:
		| @@ -4,13 +4,8 @@ import { | ||||
| 	CallExpression, | ||||
| 	Node, | ||||
| 	SyntaxKind, | ||||
| 	StringLiteral, | ||||
| 	isStringLiteralLike, | ||||
| 	isBinaryExpression, | ||||
| 	isTemplateLiteralToken, | ||||
| 	isArrayLiteralExpression | ||||
| 	StringLiteral | ||||
| } from 'typescript'; | ||||
| import { yellow } from 'colorette'; | ||||
|  | ||||
| export abstract class AbstractAstParser { | ||||
|  | ||||
| @@ -23,33 +18,16 @@ export abstract class AbstractAstParser { | ||||
| 	/** | ||||
| 	 * Get strings from function call's first argument | ||||
| 	 */ | ||||
| 	protected getCallArgStrings(callNode: CallExpression): string[] { | ||||
| 	protected getStringLiterals(callNode: CallExpression): string[] { | ||||
| 		if (!callNode.arguments.length) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		const node = callNode.arguments[0]; | ||||
|  | ||||
| 		if (isStringLiteralLike(node) || isTemplateLiteralToken(node)) { | ||||
| 			return [node.text]; | ||||
| 		} | ||||
|  | ||||
| 		if (isArrayLiteralExpression(node)) { | ||||
| 			return node.elements | ||||
| 				.map((element: StringLiteral) => element.text); | ||||
| 		} | ||||
|  | ||||
| 		if (isBinaryExpression(node)) { | ||||
| 			return [node.right] | ||||
| 				.filter(childNode => isStringLiteralLike(childNode)) | ||||
| 				.map((childNode: StringLiteral) => childNode.text); | ||||
| 		} | ||||
|  | ||||
| 		console.log(yellow(`Unsupported syntax kind in line %d: %s`), this.getLineNumber(node), this.syntaxKindToName(node.kind)); | ||||
|  | ||||
| 			return[]; | ||||
| 		} | ||||
|  | ||||
| 		const firstArg = callNode.arguments[0]; | ||||
| 		return this.findNodes(firstArg, SyntaxKind.StringLiteral) | ||||
| 			.map((node: StringLiteral) => node.text); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Find all child nodes of a kind | ||||
| 	 */ | ||||
| @@ -62,27 +40,4 @@ export abstract class AbstractAstParser { | ||||
| 		}, initialValue); | ||||
| 	} | ||||
|  | ||||
| 	protected getLineNumber(node: Node): number { | ||||
| 		const { line } = this.sourceFile.getLineAndCharacterOfPosition(node.pos); | ||||
| 		return line + 1; | ||||
| 	} | ||||
|  | ||||
| 	protected syntaxKindToName(kind: SyntaxKind): string { | ||||
| 		return SyntaxKind[kind]; | ||||
| 	} | ||||
|  | ||||
| 	protected printAllChildren(sourceFile: SourceFile, node: Node, depth = 0): void { | ||||
| 		console.log( | ||||
| 			new Array(depth + 1).join('----'), | ||||
| 			`[${node.kind}]`, | ||||
| 			this.syntaxKindToName(node.kind), | ||||
| 			`[pos: ${node.pos}-${node.end}]`, | ||||
| 			':\t\t\t', | ||||
| 			node.getFullText(sourceFile).trim() | ||||
| 		); | ||||
|  | ||||
| 		depth++; | ||||
| 		node.getChildren(sourceFile).forEach(childNode => this.printAllChildren(sourceFile, childNode, depth)); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { Node, CallExpression, SyntaxKind, Identifier } from 'typescript'; | ||||
|  | ||||
| import { ParserInterface } from './parser.interface'; | ||||
| import { AbstractAstParser } from './abstract-ast.parser'; | ||||
| import { TranslationCollection } from '../utils/translation.collection'; | ||||
|  | ||||
| import * as ts from 'typescript'; | ||||
|  | ||||
| export class FunctionParser extends AbstractAstParser implements ParserInterface { | ||||
|  | ||||
| 	protected functionIdentifier: string = '_'; | ||||
| @@ -22,24 +22,23 @@ export class FunctionParser extends AbstractAstParser implements ParserInterface | ||||
|  | ||||
| 		const callNodes = this.findCallNodes(); | ||||
| 		callNodes.forEach(callNode => { | ||||
| 			const keys: string[] = this.getCallArgStrings(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?: ts.Node): ts.CallExpression[] { | ||||
| 	protected findCallNodes(node?: Node): CallExpression[] { | ||||
| 		if (!node) { | ||||
| 			node = this.sourceFile; | ||||
| 		} | ||||
|  | ||||
| 		let callNodes = this.findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[]; | ||||
| 		let callNodes = this.findNodes(node, SyntaxKind.CallExpression) as CallExpression[]; | ||||
| 		callNodes = callNodes | ||||
| 			.filter(callNode => { | ||||
| 				// Only call expressions with arguments | ||||
| @@ -47,7 +46,7 @@ export class FunctionParser extends AbstractAstParser implements ParserInterface | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				const identifier = (callNode.getChildAt(0) as ts.Identifier).text; | ||||
| 				const identifier = (callNode.getChildAt(0) as Identifier).text; | ||||
| 				if (identifier !== this.functionIdentifier) { | ||||
| 					return false; | ||||
| 				} | ||||
|   | ||||
| @@ -1,12 +1,22 @@ | ||||
| import { | ||||
| 	SourceFile, | ||||
| 	Node, | ||||
| 	ConstructorDeclaration, | ||||
| 	Identifier, | ||||
| 	TypeReferenceNode, | ||||
| 	ClassDeclaration, | ||||
| 	SyntaxKind, | ||||
| 	CallExpression, | ||||
| 	PropertyAccessExpression | ||||
| } from 'typescript'; | ||||
|  | ||||
| import { ParserInterface } from './parser.interface'; | ||||
| import { AbstractAstParser } from './abstract-ast.parser'; | ||||
| import { TranslationCollection } from '../utils/translation.collection'; | ||||
|  | ||||
| import * as ts from 'typescript'; | ||||
|  | ||||
| export class ServiceParser extends AbstractAstParser implements ParserInterface { | ||||
|  | ||||
| 	protected sourceFile: ts.SourceFile; | ||||
| 	protected sourceFile: SourceFile; | ||||
|  | ||||
| 	public extract(contents: string, path?: string): TranslationCollection { | ||||
| 		let collection: TranslationCollection = new TranslationCollection(); | ||||
| @@ -26,7 +36,7 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
|  | ||||
| 			const callNodes = this.findCallNodes(classNode, propertyName); | ||||
| 			callNodes.forEach(callNode => { | ||||
| 				const keys: string[] = this.getCallArgStrings(callNode); | ||||
| 				const keys: string[] = this.getStringLiterals(callNode); | ||||
| 				if (keys && keys.length) { | ||||
| 					collection = collection.addKeys(keys); | ||||
| 				} | ||||
| @@ -40,7 +50,7 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
| 	 * Detect what the TranslateService instance property | ||||
| 	 * is called by inspecting constructor arguments | ||||
| 	 */ | ||||
| 	protected findTranslateServicePropertyName(constructorNode: ts.ConstructorDeclaration): string { | ||||
| 	protected findTranslateServicePropertyName(constructorNode: ConstructorDeclaration): string { | ||||
| 		if (!constructorNode) { | ||||
| 			return null; | ||||
| 		} | ||||
| @@ -57,7 +67,7 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
| 			} | ||||
|  | ||||
| 			// Make sure className is of the correct type | ||||
| 			const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier; | ||||
| 			const parameterType: Identifier = (parameter.type as TypeReferenceNode).typeName as Identifier; | ||||
| 			if (!parameterType) { | ||||
| 				return false; | ||||
| 			} | ||||
| @@ -70,22 +80,22 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
| 		}); | ||||
|  | ||||
| 		if (result) { | ||||
| 			return (result.name as ts.Identifier).text; | ||||
| 			return (result.name as Identifier).text; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Find class nodes | ||||
| 	 */ | ||||
| 	protected findClassNodes(node: ts.Node): ts.ClassDeclaration[] { | ||||
| 		return this.findNodes(node, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration[]; | ||||
| 	protected findClassNodes(node: Node): ClassDeclaration[] { | ||||
| 		return this.findNodes(node, SyntaxKind.ClassDeclaration) as ClassDeclaration[]; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Find constructor | ||||
| 	 */ | ||||
| 	protected findConstructorNode(node: ts.ClassDeclaration): ts.ConstructorDeclaration { | ||||
| 		const constructorNodes = this.findNodes(node, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration[]; | ||||
| 	protected findConstructorNode(node: ClassDeclaration): ConstructorDeclaration { | ||||
| 		const constructorNodes = this.findNodes(node, SyntaxKind.Constructor) as ConstructorDeclaration[]; | ||||
| 		if (constructorNodes) { | ||||
| 			return constructorNodes[0]; | ||||
| 		} | ||||
| @@ -94,8 +104,8 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
| 	/** | ||||
| 	 * Find all calls to TranslateService methods | ||||
| 	 */ | ||||
| 	protected findCallNodes(node: ts.Node, propertyIdentifier: string): ts.CallExpression[] { | ||||
| 		let callNodes = this.findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[]; | ||||
| 	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 | ||||
| @@ -103,19 +113,19 @@ export class ServiceParser extends AbstractAstParser implements ParserInterface | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression; | ||||
| 				if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) { | ||||
| 				const propAccess = callNode.getChildAt(0).getChildAt(0) as PropertyAccessExpression; | ||||
| 				if (!propAccess || propAccess.kind !== SyntaxKind.PropertyAccessExpression) { | ||||
| 					return false; | ||||
| 				} | ||||
| 				if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) { | ||||
| 				if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== SyntaxKind.ThisKeyword) { | ||||
| 					return false; | ||||
| 				} | ||||
| 				if (propAccess.name.text !== propertyIdentifier) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression; | ||||
| 				if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) { | ||||
| 				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')) { | ||||
|   | ||||
| @@ -19,9 +19,11 @@ describe('FunctionParser', () => { | ||||
| 			_('Hello world'); | ||||
| 			_(['I', 'am', 'extracted']); | ||||
| 			otherFunction('But I am not'); | ||||
| 			_(message || 'binary expression'); | ||||
| 			_(message ? message : 'conditional operator'); | ||||
| 		`; | ||||
| 		const keys = parser.extract(contents, componentFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted']); | ||||
| 		expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted', 'binary expression', 'conditional operator']); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -16,6 +16,34 @@ describe('ServiceParser', () => { | ||||
| 		parser = new TestServiceParser(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should support extracting binary expressions', () => { | ||||
| 		const contents = ` | ||||
| 			@Component({ }) | ||||
| 			export class AppComponent { | ||||
| 				public constructor(protected _translateService: TranslateService) { } | ||||
| 				public test() { | ||||
| 					const message = 'The Message'; | ||||
| 					this._translateService.get(message || 'Fallback message'); | ||||
| 				} | ||||
| 		`; | ||||
| 		const keys = parser.extract(contents, componentFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['Fallback message']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should support conditional operator', () => { | ||||
| 		const contents = ` | ||||
| 			@Component({ }) | ||||
| 			export class AppComponent { | ||||
| 				public constructor(protected _translateService: TranslateService) { } | ||||
| 				public test() { | ||||
| 					const message = 'The Message'; | ||||
| 					this._translateService.get(message ? message : 'Fallback message'); | ||||
| 				} | ||||
| 		`; | ||||
| 		const keys = parser.extract(contents, componentFilename).keys(); | ||||
| 		expect(keys).to.deep.equal(['Fallback message']); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract strings in TranslateService\'s get() method', () => { | ||||
| 		const contents = ` | ||||
| 			@Component({ }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user