Skip to content

[interop] Implement Diagnostics and Handling Errors #426

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

Merged
merged 2 commits into from
Jul 23, 2025
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
11 changes: 11 additions & 0 deletions web_generator/lib/src/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ Future<void> compileDartMain({String? langVersion, String? dir}) async {
);
}

Future<Process> runProcWithResult(String executable, List<String> arguments,
{required String workingDirectory}) async {
print(ansi.styleBold.wrap(['*', executable, ...arguments].join(' ')));
return Process.start(
executable,
arguments,
runInShell: Platform.isWindows,
workingDirectory: workingDirectory,
);
}

Future<void> runProc(String executable, List<String> arguments,
{required String workingDirectory, bool detached = false}) async {
print(ansi.styleBold.wrap(['*', executable, ...arguments].join(' ')));
Expand Down
27 changes: 27 additions & 0 deletions web_generator/lib/src/interop_gen/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:js_interop';

import '../js/node.dart';
import '../js/typescript.dart' as ts;

class ParserResult {
Expand All @@ -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<String> files) {
final program = ts.createProgram(files.jsify() as JSArray<JSString>,
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);
}
6 changes: 6 additions & 0 deletions web_generator/lib/src/js/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
61 changes: 61 additions & 0 deletions web_generator/lib/src/js/typescript.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ library;

import 'dart:js_interop';

import 'package:meta/meta.dart';

import 'typescript.types.dart';

@JS()
Expand All @@ -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});
Expand All @@ -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<TSDiagnosticWithLocation> getSyntacticDiagnostics([
TSSourceFile? sourceFile,
]);

/// Diagnostics related to type-checking
external JSArray<TSDiagnostic> getSemanticDiagnostics([
TSSourceFile? sourceFile,
]);

/// Diagnostics related to the .d.ts file itself
external JSArray<TSDiagnosticWithLocation> 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<TSDiagnosticRelatedInformation>? 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')
Expand All @@ -62,3 +117,9 @@ extension type TSNodeArrayCallback<T extends JSAny>._(JSObject _)
implements JSObject {
external T? call(TSNodeArray<TSNode> nodes);
}

@JS('LineAndCharacter')
extension type TSLineAndCharacter._(JSObject _) implements JSObject {
external JSNumber get line;
external JSNumber get character;
}
12 changes: 12 additions & 0 deletions web_generator/test/assets/invalid.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
49 changes: 49 additions & 0 deletions web_generator/test/invalid_input_test.dart
Original file line number Diff line number Diff line change
@@ -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));
});
});
}