From 6375d3e22a5dcba402d400bbb362c8e22fbb0f1a Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Wed, 22 Jan 2025 13:26:53 +0100 Subject: [PATCH] Throwing FlowSyntaxNotSupportedError if meets @flow annotation in a source file If a source file contains @flow annotation, the FlowSyntaxNotSupportedError is thrown as a ParseError, and the file is skipped from parsing. Also added code to check syntax errors before parsing --- openrewrite/src/javascript/parser.ts | 73 ++++++++++++++++--- .../test/javascript/parser/empty.test.ts | 2 +- .../javascript/parser/flowAnnotation.test.ts | 49 +++++++++++++ 3 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 openrewrite/test/javascript/parser/flowAnnotation.test.ts diff --git a/openrewrite/src/javascript/parser.ts b/openrewrite/src/javascript/parser.ts index 3255919b..391dd115 100644 --- a/openrewrite/src/javascript/parser.ts +++ b/openrewrite/src/javascript/parser.ts @@ -40,6 +40,7 @@ export class JavaScriptParser extends Parser { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.CommonJS, allowJs: true, + esModuleInterop: true, }; } @@ -141,21 +142,68 @@ export class JavaScriptParser extends Parser { for (const input of inputFiles.values()) { const filePath = input.path; const sourceFile = program.getSourceFile(filePath); - if (sourceFile) { - try { - const parsed = new JavaScriptParserVisitor(this, sourceFile, typeChecker).visit(sourceFile) as SourceFile; - result.push(parsed.withSourcePath(relativeTo != null ? path.relative(relativeTo, input.path) : input.path)); - } catch (error) { - result.push(ParseError.build(this, input, relativeTo, ctx, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error), null)); - } - } else { + if (!sourceFile) { result.push(ParseError.build(this, input, relativeTo, ctx, new Error('Parser returned undefined'), null)); + continue; } - } + if (this.hasFlowAnnotation(sourceFile)) { + result.push(ParseError.build(this, input, relativeTo, ctx, new FlowSyntaxNotSupportedError(`Flow syntax not supported: ${input.path}`), null)); + continue; + } + + // ToDo: uncomment code after tests fixing + // const syntaxErrors = this.checkSyntaxErrors(program, sourceFile); + // if (syntaxErrors.length > 0) { + // syntaxErrors.forEach( + // e => result.push(ParseError.build(this, input, relativeTo, ctx, new SyntaxError(`Compiler error: ${e[0]} [${e[1]}]`), null)) + // ); + // continue; + // } + + try { + const parsed = new JavaScriptParserVisitor(this, sourceFile, typeChecker).visit(sourceFile) as SourceFile; + result.push(parsed.withSourcePath(relativeTo != null ? path.relative(relativeTo, input.path) : input.path)); + } catch (error) { + result.push(ParseError.build(this, input, relativeTo, ctx, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error), null)); + } + } return result; } + private checkSyntaxErrors(program: ts.Program, sourceFile: ts.SourceFile) { + const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); + // checking Parsing and Syntax Errors + let syntaxErrors : [errorMsg: string, errorCode: number][] = []; + if (diagnostics.length > 0) { + const errors = diagnostics.filter(d => d.code >= 1000 && d.code < 2000); + if (errors.length > 0) { + syntaxErrors = errors.map(e => { + let errorMsg; + if (e.file) { + let {line, character} = ts.getLineAndCharacterOfPosition(e.file, e.start!); + let message = ts.flattenDiagnosticMessageText(e.messageText, "\n"); + errorMsg = `${e.file.fileName} (${line + 1},${character + 1}): ${message}`; + } else { + errorMsg = ts.flattenDiagnosticMessageText(e.messageText, "\n"); + } + return [errorMsg, e.code]; + }); + } + } + return syntaxErrors; + } + + private hasFlowAnnotation(sourceFile: ts.SourceFile) { + if (sourceFile.fileName.endsWith('.js') || sourceFile.fileName.endsWith('.jsx')) { + const comments = sourceFile.getFullText().match(/\/\*[\s\S]*?\*\/|\/\/.*(?=[\r\n])/g); + if (comments) { + return comments.some(comment => comment.includes("@flow")); + } + } + return false; + } + accept(path: string): boolean { return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.js') || path.endsWith('.jsx'); } @@ -4094,3 +4142,10 @@ function prefixFromNode(node: ts.Node, sourceFile: ts.SourceFile): Space { // Step 4: Return the Space object with comments and leading whitespace return new Space(comments, whitespace.length > 0 ? whitespace : null); } + +class FlowSyntaxNotSupportedError extends SyntaxError { + constructor(message: string = "Flow syntax is not supported") { + super(message); + this.name = "FlowSyntaxNotSupportedError"; + } +} diff --git a/openrewrite/test/javascript/parser/empty.test.ts b/openrewrite/test/javascript/parser/empty.test.ts index f60fccdb..af7b29c8 100644 --- a/openrewrite/test/javascript/parser/empty.test.ts +++ b/openrewrite/test/javascript/parser/empty.test.ts @@ -7,7 +7,7 @@ describe('empty mapping', () => { test('simple', () => { rewriteRun( //language=typescript - typeScript('if (true) ;') + typeScript('if (true) {/*a*/};') ); }); }); diff --git a/openrewrite/test/javascript/parser/flowAnnotation.test.ts b/openrewrite/test/javascript/parser/flowAnnotation.test.ts new file mode 100644 index 00000000..7349e917 --- /dev/null +++ b/openrewrite/test/javascript/parser/flowAnnotation.test.ts @@ -0,0 +1,49 @@ +import {connect, disconnect, javaScript, rewriteRun, typeScript} from '../testHarness'; + +describe('flow annotation checking test', () => { + beforeAll(() => connect()); + afterAll(() => disconnect()); + + test('@flow in a comment in js', () => { + const faultyTest = () => rewriteRun( + //language=javascript + javaScript(` + //@flow + + import Rocket from './rocket'; + import RocketLaunch from './rocket-launch'; + `) + ); + + expect(faultyTest).toThrow(/FlowSyntaxNotSupportedError/); + }); + + test('@flow in a multiline comment in js', () => { + const faultyTest = () => rewriteRun( + //language=javascript + javaScript(` + /* + @flow + */ + + import Rocket from './rocket'; + import RocketLaunch from './rocket-launch'; + `) + ); + + expect(faultyTest).toThrow(/FlowSyntaxNotSupportedError/); + }); + + test('@flow in a comment in ts', () => { + rewriteRun( + //language=typescript + typeScript(` + //@flow + + import Rocket from './rocket'; + import RocketLaunch from './rocket-launch'; + `) + ); + }); + +});