(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, 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,31 +18,14 @@ 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; return[];
} }
const node = callNode.arguments[0]; const firstArg = callNode.arguments[0];
return this.findNodes(firstArg, SyntaxKind.StringLiteral)
if (isStringLiteralLike(node) || isTemplateLiteralToken(node)) { .map((node: StringLiteral) => node.text);
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 [];
} }
/** /**
@ -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));
}
} }

View File

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

View File

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

View File

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

View File

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