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

fix: Fix service and function parsing when used after bracket syntax casting expression #51

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
Loading