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