(refactor) simplify extraction of string literals

This commit is contained in:
Kim Biesbjerg 2019-06-13 11:23:37 +02:00
parent 02fc705bc0
commit 4fd7efa2dc
5 changed files with 71 additions and 77 deletions

View File

@ -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,31 +18,14 @@ 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;
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);
}
/**
@ -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));
}
}

View File

@ -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;
}

View File

@ -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')) {

View File

@ -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']);
});
});

View File

@ -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({ })