Repository: vendure-ecommerce/ngx-translate-extract (ts)

Issue:
Translation ignored in ts with <any> cast
Hello, I recently upgraded from 8.3.0 to 9.1.1 and some keys went missing with the new extraction, after some research it looks like this line of code makes the parser crash in a .ts file

`if (<any>this.editor) (<any>this.editor)._editor.updateOptions({ readOnly: this._disabled });`

In this context : 
![image](https://github.com/vendure-ecommerce/ngx-translate-extract/assets/10205672/de16b49c-e4b5-4afe-8a80-4f922f5bab6a)

It seems related to #43 

I saw that the 9.1.1 version was published 3month ago so just tried with 9.2.0-next.0 and the bug is still present

(I'd love a new preview release of 9.2.0 if you fix it so that I can swap to the newer version of the tool, we recently upgraded to angular 18 and started using the new control flow syntax :) Thanks ! )

Pull request description:
fix: Fix service and function parsing when used after bracket syntax casting expression
This is a followup fix from #43 which was only addressing the issue for the `marker` parser. This change also fixes the service and function parser.

Fixes #50

Patch:
diff --git a/src/parsers/function.parser.ts b/src/parsers/function.parser.ts
index cfee515..85b2c52 100644
--- a/src/parsers/function.parser.ts
+++ b/src/parsers/function.parser.ts
@@ -1,8 +1,6 @@
-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;
 
@@ -10,7 +8,7 @@ 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();
 
diff --git a/src/parsers/marker.parser.ts b/src/parsers/marker.parser.ts
index c9812da..0d1e339 100644
--- a/src/parsers/marker.parser.ts
+++ b/src/parsers/marker.parser.ts
@@ -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) {
diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts
index d2cbb21..349488f 100644
--- a/src/parsers/service.parser.ts
+++ b/src/parsers/service.parser.ts
@@ -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';
@@ -19,7 +18,8 @@ import {
 	getSuperClassName,
 	getImportPath,
 	findFunctionExpressions,
-	findVariableNameByInjectType
+	findVariableNameByInjectType,
+	getAST
 } from '../utils/ast-helpers.js';
 
 const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
@@ -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);
 
@@ -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));
diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts
index 6c9e6ee..9df125f 100644
--- a/src/utils/ast-helpers.ts
+++ b/src/utils/ast-helpers.ts
@@ -1,4 +1,5 @@
-import { tsquery } from '@phenomnomnominal/tsquery';
+import { extname } from 'node:path';
+import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
 import pkg, {
 	Node,
 	NamedImports,
@@ -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);


Test patch:
diff --git a/tests/parsers/function.parser.spec.ts b/tests/parsers/function.parser.spec.ts
index eb4bc66..c87050f 100644
--- a/tests/parsers/function.parser.spec.ts
+++ b/tests/parsers/function.parser.spec.ts
@@ -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']);
+	});
 });
diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts
index 66bcc86..ea7ee3e 100644
--- a/tests/parsers/service.parser.spec.ts
+++ b/tests/parsers/service.parser.spec.ts
@@ -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 = `
diff --git a/tests/utils/ast-helpers.spec.ts b/tests/utils/ast-helpers.spec.ts
new file mode 100644
index 0000000..053214a
--- /dev/null
+++ b/tests/utils/ast-helpers.spec.ts
@@ -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);
+	});
+});