Skip to content

Commit

Permalink
fix: Fix service and function parsing when used after bracket syntax …
Browse files Browse the repository at this point in the history
…casting expression (#51)

Fixes #50
  • Loading branch information
pmpak authored Jun 10, 2024
1 parent 8547433 commit bd8f4e6
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 25 deletions.
6 changes: 2 additions & 4 deletions src/parsers/function.parser.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { tsquery } from '@phenomnomnominal/tsquery';

import { ParserInterface } from './parser.interface.js';
import { TranslationCollection } from '../utils/translation.collection.js';
import { getStringsFromExpression, findSimpleCallExpressions } from '../utils/ast-helpers.js';
import { getStringsFromExpression, findSimpleCallExpressions, getAST } from '../utils/ast-helpers.js';
import pkg from 'typescript';
const { isIdentifier } = pkg;

export class FunctionParser implements ParserInterface {
constructor(private fnName: string) {}

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

let collection: TranslationCollection = new TranslationCollection();

Expand Down
16 changes: 2 additions & 14 deletions src/parsers/marker.parser.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
import { extname } from 'path';

import { ParserInterface } from './parser.interface.js';
import { TranslationCollection } from '../utils/translation.collection.js';
import { getNamedImportAlias, findFunctionCallExpressions, getStringsFromExpression } from '../utils/ast-helpers.js';
import { getNamedImportAlias, findFunctionCallExpressions, getStringsFromExpression, getAST } from '../utils/ast-helpers.js';

const MARKER_MODULE_NAME = 'ngx-translate-extract-marker';
const MARKER_IMPORT_NAME = 'marker';

export class MarkerParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
const supportedScriptTypes: Record<string, ScriptKind> = {
'.js': ScriptKind.JS,
'.jsx': ScriptKind.JSX,
'.ts': ScriptKind.TS,
'.tsx': ScriptKind.TSX
};

const scriptKind = supportedScriptTypes[extname(filePath)] ?? ScriptKind.TS;

const sourceFile = tsquery.ast(source, filePath, scriptKind);
const sourceFile = getAST(source, filePath);

const markerImportName = getNamedImportAlias(sourceFile, MARKER_MODULE_NAME, MARKER_IMPORT_NAME);
if (!markerImportName) {
Expand Down
8 changes: 4 additions & 4 deletions src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import fs from 'node:fs';
import { ClassDeclaration, CallExpression, SourceFile } from 'typescript';
import { resolveSync } from 'tsconfig';
import JSON5 from 'json5';
import { tsquery } from '@phenomnomnominal/tsquery';

import { ParserInterface } from './parser.interface.js';
import { TranslationCollection } from '../utils/translation.collection.js';
Expand All @@ -19,7 +18,8 @@ import {
getSuperClassName,
getImportPath,
findFunctionExpressions,
findVariableNameByInjectType
findVariableNameByInjectType,
getAST
} from '../utils/ast-helpers.js';

const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
Expand All @@ -29,7 +29,7 @@ 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);
const sourceFile = getAST(source, filePath);
const classDeclarations = findClassDeclarations(sourceFile);
const functionDeclarations = findFunctionExpressions(sourceFile);

Expand Down Expand Up @@ -141,7 +141,7 @@ export class ServiceParser implements ParserInterface {
const allSuperClassPropertyNames: string[] = [];
potentialSuperFiles.forEach((file) => {
const superClassFileContent = fs.readFileSync(file, 'utf8');
const superClassAst = tsquery.ast(superClassFileContent, file);
const superClassAst = getAST(superClassFileContent, file);
const superClassDeclarations = findClassDeclarations(superClassAst, superClassName);
const superClassPropertyNames = superClassDeclarations
.flatMap((superClassDeclaration) => findClassPropertiesByType(superClassDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE));
Expand Down
20 changes: 17 additions & 3 deletions src/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import { extname } from 'node:path';
import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
import pkg, {
Node,
NamedImports,
Expand All @@ -7,11 +8,24 @@ import pkg, {
ConstructorDeclaration,
CallExpression,
Expression,
PropertyAccessExpression,
StringLiteral
StringLiteral,
SourceFile
} from 'typescript';
const { SyntaxKind, isStringLiteralLike, isArrayLiteralExpression, isBinaryExpression, isConditionalExpression } = pkg;

export function getAST(source: string, fileName = ''): SourceFile {
const supportedScriptTypes: Record<string, ScriptKind> = {
'.js': ScriptKind.JS,
'.jsx': ScriptKind.JSX,
'.ts': ScriptKind.TS,
'.tsx': ScriptKind.TSX
};

const scriptKind = supportedScriptTypes[extname(fileName)] ?? ScriptKind.TS;

return tsquery.ast(source, fileName, scriptKind);
}

export function getNamedImports(node: Node, moduleName: string): NamedImports[] {
const query = `ImportDeclaration[moduleSpecifier.text=/${moduleName}/] NamedImports`;
return tsquery<NamedImports>(node, query);
Expand Down
16 changes: 16 additions & 0 deletions tests/parsers/function.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ describe('FunctionParser', () => {
expect(keys).to.deep.equal(['DYNAMIC_TRAD.val1', 'DYNAMIC_TRAD.val2']);
});

it('should not break after bracket syntax casting', () => {
const contents = `
export class AppModule {
constructor() {
const input: unknown = 'hello';
const myNiceVar1 = input as string;
MK('hello.after.as.syntax');
const myNiceVar2 = <string>input;
MK('hello.after.bracket.syntax');
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([ 'hello.after.as.syntax', 'hello.after.bracket.syntax']);
});
});
23 changes: 23 additions & 0 deletions tests/parsers/service.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,29 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal([]);
});

it('should not break after bracket syntax casting', () => {
const contents = `
@Component({ })
export class AppComponent {
@Input()
set color(value: unknown) {
const newValue = <string>value;
this._color = value;
this._translateService.instant('hello.from.input.setter');
}
_color: unknown;
constructor(protected _translateService: TranslateService) { }
method() {
this._translateService.instant('hello.from.method');
}
`;
const keys = parser.extract(contents, componentFilename)?.keys();
expect(keys).to.deep.equal(['hello.from.input.setter', 'hello.from.method']);
});

describe('function expressions', () => {
it('should extract from arrow function expression', () => {
const contents = `
Expand Down
73 changes: 73 additions & 0 deletions tests/utils/ast-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { LanguageVariant } from 'typescript';

import { getAST } from '../../src/utils/ast-helpers';

describe('getAST()', () => {
const tsqueryAstSpy = vi.spyOn(tsquery, 'ast');

beforeEach(() => {
tsqueryAstSpy.mockClear();
});

it('should return the AST for a TypeScript source with a .ts file extension', () => {
const source = 'const x: number = 42;';
const fileName = 'example.ts';

const result = getAST(source, fileName);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, fileName, ScriptKind.TS);
expect(result.languageVariant).toBe(LanguageVariant.Standard);
});

it('should return the AST for a TypeScript source with a .tsx file extension', () => {
const source = 'const x: number = 42;';
const fileName = 'example.tsx';

const result = getAST(source, fileName);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, fileName, ScriptKind.TSX);
expect(result.languageVariant).toBe(LanguageVariant.JSX);
});

it('should return the AST for a JavaScript source with a .js file extension', () => {
const source = 'const x = 42;';
const fileName = 'example.js';

const result = getAST(source, fileName);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, fileName, ScriptKind.JS);
// JS files also return JSX language variant.
expect(result.languageVariant).toBe(LanguageVariant.JSX);
});

it('should return the AST for a JavaScript source with a .jsx file extension', () => {
const source = 'const x = 42;';
const fileName = 'example.jsx';

const result = getAST(source, fileName);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, fileName, ScriptKind.JSX);
expect(result.languageVariant).toBe(LanguageVariant.JSX);
});

it('should use ScriptKind.TS if the file extension is unsupported', () => {
const source = 'const x: number = 42;';
const fileName = 'example.unknown';

const result = getAST(source, fileName);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, fileName, ScriptKind.TS);
expect(result.languageVariant).toBe(LanguageVariant.Standard);
});

it('should use ScriptKind.TS if no file name is provided', () => {
const source = 'const x: number = 42;';

const result = getAST(source);

expect(tsqueryAstSpy).toHaveBeenCalledWith(source, '', ScriptKind.TS);
expect(result.languageVariant).toBe(LanguageVariant.Standard);
});
});

0 comments on commit bd8f4e6

Please sign in to comment.