Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several fixes #21

Merged
merged 13 commits into from
Sep 21, 2023
66 changes: 61 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
"dependencies": {
"@phenomnomnominal/tsquery": "^5.0.1",
"boxen": "^6.2.1",
"braces": "^3.0.2",
"colorette": "^2.0.16",
"flat": "^5.0.2",
"gettext-parser": "^4.2.0",
"glob": "^7.2.0",
"path": "^0.12.7",
"terminal-link": "^3.0.0",
"yargs": "^17.5.1",
"braces": "^3.0.2"
"tsconfig": "^7.0.0",
"yargs": "^17.5.1"
},
"devDependencies": {
"@types/braces": "^3.0.1",
Expand Down
91 changes: 83 additions & 8 deletions src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { ClassDeclaration, CallExpression } from 'typescript';
import { ClassDeclaration, CallExpression, StringLiteral, SourceFile } from 'typescript';
import { tsquery } from '@phenomnomnominal/tsquery';

import { ParserInterface } from './parser.interface.js';
import { TranslationCollection } from '../utils/translation.collection.js';
import {
findClassDeclarations,
findClassPropertyByType,
findClassPropertiesByType,
findPropertyCallExpressions,
findMethodCallExpressions,
getStringsFromExpression,
findMethodParameterByType,
findConstructorDeclaration
findConstructorDeclaration,
getSuperClassName,
getImportPath
} from '../utils/ast-helpers.js';
import * as path from 'path';
import * as fs from 'fs';
import { loadSync } from 'tsconfig';

const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];

export class ServiceParser implements ParserInterface {
private static propertyMap = new Map<string, string[]>();

public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);

Expand All @@ -30,7 +37,7 @@ export class ServiceParser implements ParserInterface {
classDeclarations.forEach((classDeclaration) => {
const callExpressions = [
...this.findConstructorParamCallExpressions(classDeclaration),
...this.findPropertyCallExpressions(classDeclaration)
...this.findPropertyCallExpressions(classDeclaration, sourceFile)
];

callExpressions.forEach((callExpression) => {
Expand All @@ -54,11 +61,79 @@ export class ServiceParser implements ParserInterface {
return findMethodCallExpressions(constructorDeclaration, paramName, TRANSLATE_SERVICE_METHOD_NAMES);
}

protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
if (!propName) {
protected findPropertyCallExpressions(classDeclaration: ClassDeclaration, sourceFile: SourceFile): CallExpression[] {
const propNames = [
...findClassPropertiesByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE),
...this.findParentClassProperties(classDeclaration, sourceFile)
];
return propNames.flatMap((name) => findPropertyCallExpressions(classDeclaration, name, TRANSLATE_SERVICE_METHOD_NAMES));
}

private findParentClassProperties(classDeclaration: ClassDeclaration, ast: SourceFile): string[] {
const superClassName = getSuperClassName(classDeclaration);
if (!superClassName) {
return [];
}
const importPath = getImportPath(ast, superClassName);
if (!importPath) {
// parent class must be in the same file and will be handled automatically, so we can
// skip it here
return [];
}

const currDir = path.join(path.dirname(ast.fileName), '/');

const key = `${currDir}|${importPath}`;
if (key in ServiceParser.propertyMap) {
return ServiceParser.propertyMap.get(key);
}

let superClassPath: string;
if (importPath.startsWith('.')) {
// relative import, use currDir
superClassPath = path.resolve(currDir, importPath);
} else if (importPath.startsWith('/')) {
// absolute relative import, use path directly
superClassPath = importPath;
} else {
// absolute import, use baseUrl if present
const config = loadSync(currDir);
let baseUrl = config?.config?.compilerOptions?.baseUrl;
if (baseUrl) {
baseUrl = path.resolve(path.dirname(config.path), baseUrl);
}
superClassPath = path.resolve(baseUrl ?? currDir, importPath);
}
const superClassFile = superClassPath + '.ts';
let potentialSuperFiles: string[];
if (fs.existsSync(superClassFile) && fs.lstatSync(superClassFile).isFile()) {
potentialSuperFiles = [superClassFile];
} else if (fs.existsSync(superClassPath) && fs.lstatSync(superClassPath).isDirectory()) {
potentialSuperFiles = fs
.readdirSync(superClassPath)
.filter((file) => file.endsWith('.ts'))
.map((file) => path.join(superClassPath, file));
} else {
// we cannot find the superclass, so just assume that no translate property exists
return [];
}
return findPropertyCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES);

const allSuperClassPropertyNames: string[] = [];
potentialSuperFiles.forEach((file) => {
const superClassFileContent = fs.readFileSync(file, 'utf8');
const superClassAst = tsquery.ast(superClassFileContent, file);
const superClassDeclarations = findClassDeclarations(superClassAst, superClassName);
const superClassPropertyNames = superClassDeclarations
.flatMap((superClassDeclaration) => findClassPropertiesByType(superClassDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE));
if (superClassPropertyNames.length > 0) {
ServiceParser.propertyMap.set(file, superClassPropertyNames);
allSuperClassPropertyNames.push(...superClassPropertyNames);
} else {
superClassDeclarations.forEach((declaration) =>
allSuperClassPropertyNames.push(...this.findParentClassProperties(declaration, superClassAst))
);
}
});
return allSuperClassPropertyNames;
}
}
82 changes: 62 additions & 20 deletions src/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import pkg, { CallExpression, ClassDeclaration, ConstructorDeclaration, Expression, Identifier, NamedImports, Node, PropertyAccessExpression } from 'typescript';

import pkg, {
Node,
NamedImports,
Identifier,
ClassDeclaration,
ConstructorDeclaration,
CallExpression,
Expression,
PropertyAccessExpression,
StringLiteral
} from 'typescript';
const { SyntaxKind, isStringLiteralLike, isArrayLiteralExpression, isBinaryExpression, isConditionalExpression } = pkg;

export function getNamedImports(node: Node, moduleName: string): NamedImports[] {
Expand All @@ -25,13 +34,32 @@ export function getNamedImportAlias(node: Node, moduleName: string, importName:
return null;
}

export function findClassDeclarations(node: Node): ClassDeclaration[] {
const query = 'ClassDeclaration';
export function findClassDeclarations(node: Node, name: string = null): ClassDeclaration[] {
let query = 'ClassDeclaration';
if (name) {
query += `:has(Identifier[name="${name}"])`;
}
return tsquery<ClassDeclaration>(node, query);
}

export function findClassPropertyByType(node: ClassDeclaration, type: string): string | null {
return findClassPropertyConstructorParameterByType(node, type) || findClassPropertyDeclarationByType(node, type);
export function getSuperClassName(node: Node): string | null {
const query = 'ClassDeclaration > HeritageClause Identifier';
const [result] = tsquery<Identifier>(node, query);
return result?.text;
}

export function getImportPath(node: Node, className: string): string | null {
const query = `ImportDeclaration:has(Identifier[name="${className}"]) StringLiteral`;
const [result] = tsquery<StringLiteral>(node, query);
return result?.text;
}

export function findClassPropertiesByType(node: ClassDeclaration, type: string): string[] {
return [
...findClassPropertiesConstructorParameterByType(node, type),
...findClassPropertiesDeclarationByType(node, type),
...findClassPropertiesGetterByType(node, type)
];
}

export function findConstructorDeclaration(node: ClassDeclaration): ConstructorDeclaration {
Expand All @@ -57,22 +85,22 @@ export function findMethodCallExpressions(node: Node, propName: string, fnName:
return tsquery<PropertyAccessExpression>(node, query).map((n) => n.parent as CallExpression);
}

export function findClassPropertyConstructorParameterByType(node: ClassDeclaration, type: string): string | null {
export function findClassPropertiesConstructorParameterByType(node: ClassDeclaration, type: string): string[] {
const query = `Constructor Parameter:has(TypeReference > Identifier[name="${type}"]):has(PublicKeyword,ProtectedKeyword,PrivateKeyword) > Identifier`;
const [result] = tsquery<Identifier>(node, query);
if (result) {
return result.text;
}
return null;
const result = tsquery<Identifier>(node, query);
return result.map((n) => n.text);
}

export function findClassPropertyDeclarationByType(node: ClassDeclaration, type: string): string | null {
export function findClassPropertiesDeclarationByType(node: ClassDeclaration, type: string): string[] {
const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const [result] = tsquery<Identifier>(node, query);
if (result) {
return result.text;
}
return null;
const result = tsquery<Identifier>(node, query);
return result.map((n) => n.text);
}

export function findClassPropertiesGetterByType(node: ClassDeclaration, type: string): string[] {
const query = `GetAccessor:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const result = tsquery<Identifier>(node, query);
return result.map((n) => n.text);
}

export function findFunctionCallExpressions(node: Node, fnName: string | string[]): CallExpression[] {
Expand All @@ -95,8 +123,22 @@ export function findPropertyCallExpressions(node: Node, prop: string, fnName: st
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${prop}"]):has(ThisKeyword))`;
return tsquery<PropertyAccessExpression>(node, query).map((n) => n.parent as CallExpression);
const query = 'CallExpression > ' +
`PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${prop}"]):has(ThisKeyword)) > ` +
`PropertyAccessExpression:has(ThisKeyword) > Identifier[name="${prop}"]`;
const nodes = tsquery<Identifier>(node, query);
// Since the direct descendant operator (>) is not supported in :has statements, we need to
// check manually whether everything is correctly matched
const filtered = nodes.reduce<CallExpression[]>((result: CallExpression[], n: Node) => {
if (
tsquery(n.parent, 'PropertyAccessExpression > ThisKeyword').length > 0 &&
tsquery(n.parent.parent, `PropertyAccessExpression > Identifier[name=/^(${fnName})$/]`).length > 0
) {
result.push(n.parent.parent.parent as CallExpression);
}
return result;
}, []);
return filtered;
}

export function getStringsFromExpression(expression: Expression): string[] {
Expand Down
Loading