diff --git a/web_generator/lib/src/cli.dart b/web_generator/lib/src/cli.dart index b81a69da..fd45c090 100644 --- a/web_generator/lib/src/cli.dart +++ b/web_generator/lib/src/cli.dart @@ -31,6 +31,17 @@ Future compileDartMain({String? langVersion, String? dir}) async { ); } +Future runProcWithResult(String executable, List arguments, + {required String workingDirectory}) async { + print(ansi.styleBold.wrap(['*', executable, ...arguments].join(' '))); + return Process.start( + executable, + arguments, + runInShell: Platform.isWindows, + workingDirectory: workingDirectory, + ); +} + Future runProc(String executable, List arguments, {required String workingDirectory, bool detached = false}) async { print(ansi.styleBold.wrap(['*', executable, ...arguments].join(' '))); diff --git a/web_generator/lib/src/interop_gen/parser.dart b/web_generator/lib/src/interop_gen/parser.dart index 8a65c099..aaf1f86c 100644 --- a/web_generator/lib/src/interop_gen/parser.dart +++ b/web_generator/lib/src/interop_gen/parser.dart @@ -4,6 +4,7 @@ import 'dart:js_interop'; +import '../js/node.dart'; import '../js/typescript.dart' as ts; class ParserResult { @@ -13,9 +14,35 @@ class ParserResult { ParserResult({required this.program, required this.files}); } +/// Parses the given TypeScript declaration [files], provides any diagnostics, +/// if any, and generates a [ts.TSProgram] for transformation ParserResult parseDeclarationFiles(Iterable files) { final program = ts.createProgram(files.jsify() as JSArray, ts.TSCompilerOptions(declaration: true)); + // get diagnostics + final diagnostics = [ + ...program.getSemanticDiagnostics().toDart, + ...program.getSyntacticDiagnostics().toDart, + ...program.getDeclarationDiagnostics().toDart, + ]; + + // handle diagnostics + for (final diagnostic in diagnostics) { + if (diagnostic.file case final diagnosticFile?) { + final ts.TSLineAndCharacter(line: line, character: char) = + ts.getLineAndCharacterOfPosition(diagnosticFile, diagnostic.start!); + final message = + ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + printErr('${diagnosticFile.fileName} ' + '(${line.toDartInt + 1},${char.toDartInt + 1}): $message'); + } + } + + if (diagnostics.isNotEmpty) { + // exit + exit(1); + } + return ParserResult(program: program, files: files); } diff --git a/web_generator/lib/src/js/node.dart b/web_generator/lib/src/js/node.dart index 4995939e..81c1ead1 100644 --- a/web_generator/lib/src/js/node.dart +++ b/web_generator/lib/src/js/node.dart @@ -6,3 +6,9 @@ import 'dart:js_interop'; @JS() external String get url; + +@JS('process.exit') +external void exit(int code); + +@JS('console.error') +external void printErr(String message); diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart index af8fd98c..2ad37dc0 100644 --- a/web_generator/lib/src/js/typescript.dart +++ b/web_generator/lib/src/js/typescript.dart @@ -7,6 +7,8 @@ library; import 'dart:js_interop'; +import 'package:meta/meta.dart'; + import 'typescript.types.dart'; @JS() @@ -30,6 +32,14 @@ external bool isTypeReferenceNode(TSNode node); @JS() external bool isThisTypeNode(TSNode node); +@JS() +external TSLineAndCharacter getLineAndCharacterOfPosition( + TSSourceFile sourceFile, int position); + +@JS() +external String flattenDiagnosticMessageText(JSAny? diag, String newLine, + [int indent]); + @JS('CompilerOptions') extension type TSCompilerOptions._(JSObject _) implements JSObject { external TSCompilerOptions({bool? allowJs, bool? declaration}); @@ -41,6 +51,51 @@ extension type TSCompilerOptions._(JSObject _) implements JSObject { extension type TSProgram._(JSObject _) implements JSObject { external TSSourceFile? getSourceFile(String file); external TSTypeChecker getTypeChecker(); + + /// Diagnostics related to syntax errors + external JSArray getSyntacticDiagnostics([ + TSSourceFile? sourceFile, + ]); + + /// Diagnostics related to type-checking + external JSArray getSemanticDiagnostics([ + TSSourceFile? sourceFile, + ]); + + /// Diagnostics related to the .d.ts file itself + external JSArray getDeclarationDiagnostics([ + TSSourceFile? sourceFile, + ]); +} + +@JS('DiagnosticRelatedInformation') +extension type TSDiagnosticRelatedInformation._(JSObject _) + implements JSObject { + external TSSourceFile? file; + external int code; + + /// [String] or `TSDiagnosticMessageChain` (unimplemented) + external JSAny messageText; + external int? start; +} + +@JS('Diagnostic') +extension type TSDiagnostic._(JSObject _) + implements TSDiagnosticRelatedInformation { + external String? source; + external JSArray? relatedInformation; + + @doNotStore + external JSObject? get reportsUnnecessary; + @doNotStore + external JSObject? get reportsDeprecated; +} + +@JS('DiagnosticWithLocation') +extension type TSDiagnosticWithLocation._(JSObject _) implements TSDiagnostic { + external TSSourceFile file; + external int start; + external int length; } @JS('TypeChecker') @@ -62,3 +117,9 @@ extension type TSNodeArrayCallback._(JSObject _) implements JSObject { external T? call(TSNodeArray nodes); } + +@JS('LineAndCharacter') +extension type TSLineAndCharacter._(JSObject _) implements JSObject { + external JSNumber get line; + external JSNumber get character; +} diff --git a/web_generator/test/assets/invalid.d.ts b/web_generator/test/assets/invalid.d.ts new file mode 100644 index 00000000..cd01dcd1 --- /dev/null +++ b/web_generator/test/assets/invalid.d.ts @@ -0,0 +1,12 @@ +// invalid-syntax.d.ts +export interface Person { + name: string + age number +} +interface User { + id: string; +} +export declare class Admin implements User { + constructor(name: string); + toString(): string; +} diff --git a/web_generator/test/invalid_input_test.dart b/web_generator/test/invalid_input_test.dart new file mode 100644 index 00000000..76654470 --- /dev/null +++ b/web_generator/test/invalid_input_test.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:convert'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:web_generator/src/cli.dart'; + +/// Actual test output can be found in `.dart_tool/idl` +void main() { + final bindingsGenPath = p.join('lib', 'src'); + group('Interop Gen Integration Test', () { + final testFile = p.join('test', 'assets', 'invalid.d.ts'); + final outputFile = p.join('.dart_tool', 'interop_gen', 'invalid.dart'); + + setUp(() async { + // set up npm + await runProc('npm', ['install'], workingDirectory: bindingsGenPath); + + // compile file + await compileDartMain(dir: bindingsGenPath); + }); + + test('Expect Parsing to Fail', () async { + final inputFilePath = p.relative(testFile, from: bindingsGenPath); + final outputFilePath = p.relative(outputFile, from: bindingsGenPath); + + final process = await runProcWithResult( + 'node', + [ + 'main.mjs', + '--input=$inputFilePath', + '--output=$outputFilePath', + '--declaration' + ], + workingDirectory: bindingsGenPath); + + final stderr = await process.stderr.transform(utf8.decoder).toList(); + + expect(stderr, isNotEmpty); + expect(await process.exitCode, isNot(0)); + }); + }); +}