[interop] Default to `JSAny` for unsupported type and skip unsupported declarations (#450)
* [interop] Default to `JSAny` for unsupported type and skip unsupported declarations
* added bypass as cmdline option only
* implemented testing
* re-added never type case
diff --git a/web_generator/bin/gen_interop_bindings.dart b/web_generator/bin/gen_interop_bindings.dart
index 6d2df55..129a5bb 100644
--- a/web_generator/bin/gen_interop_bindings.dart
+++ b/web_generator/bin/gen_interop_bindings.dart
@@ -89,6 +89,7 @@
if (relativeConfigFile case final config?) '--config=$config',
if (argResult.wasParsed('ignore-errors')) '--ignore-errors',
if (argResult.wasParsed('generate-all')) '--generate-all',
+ if (argResult.wasParsed('strict-unsupported')) '--strict-unsupported',
],
workingDirectory: bindingsGeneratorPath,
);
@@ -120,6 +121,11 @@
help: 'Generate all declarations '
'(including private declarations)',
negatable: false)
+ ..addFlag('strict-unsupported',
+ help:
+ 'Treat unsupported declarations/types as errors. Only used for development of the generator',
+ negatable: false,
+ hide: true)
..addOption('config',
hide: true,
abbr: 'c',
diff --git a/web_generator/lib/src/config.dart b/web_generator/lib/src/config.dart
index a20227a..1d83d0e 100644
--- a/web_generator/lib/src/config.dart
+++ b/web_generator/lib/src/config.dart
@@ -66,6 +66,12 @@
/// declarations
bool get generateAll;
+ /// Treat any unsupported/unimplemented types/declarations as errors
+ ///
+ /// This is to be used for development purposes only and is not supported
+ /// as a config option in the configuration file
+ bool get strictUnsupported;
+
factory Config(
{required List<String> input,
required String output,
@@ -75,7 +81,8 @@
List<String> includedDeclarations,
bool generateAll,
bool ignoreErrors,
- String? tsConfigFile}) = ConfigImpl._;
+ String? tsConfigFile,
+ bool strictUnsupported}) = ConfigImpl._;
}
class ConfigImpl implements Config {
@@ -118,6 +125,9 @@
@override
bool generateAll;
+ @override
+ bool strictUnsupported;
+
ConfigImpl._(
{required this.input,
required this.output,
@@ -127,7 +137,8 @@
this.includedDeclarations = const [],
this.ignoreErrors = false,
this.generateAll = false,
- this.tsConfigFile});
+ this.tsConfigFile,
+ this.strictUnsupported = false});
@override
bool get singleFileOutput => input.length == 1;
@@ -176,6 +187,9 @@
@override
bool generateAll;
+ @override
+ bool get strictUnsupported => false;
+
YamlConfig._(
{required this.filename,
required this.input,
diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart
index 2f5a8ef..ce5bd4e 100644
--- a/web_generator/lib/src/dart_main.dart
+++ b/web_generator/lib/src/dart_main.dart
@@ -62,7 +62,8 @@
languageVersion: Version.parse(languageVersionString),
tsConfigFile: tsConfigFile,
ignoreErrors: argResult.wasParsed('ignore-errors'),
- generateAll: argResult['generate-all'] as bool);
+ generateAll: argResult['generate-all'] as bool,
+ strictUnsupported: argResult.wasParsed('strict-unsupported'));
}
await generateJSInteropBindings(config);
@@ -202,5 +203,11 @@
help: '[TS Declarations] The input file to read and generate types for')
..addFlag('ignore-errors',
help: '[TS Declarations] Ignore Generator Errors', negatable: false)
+ ..addFlag(
+ 'strict-unsupported',
+ help:
+ '[TS Declarations] Treat unsupported declarations/types as errors. Only used for development of the generator',
+ negatable: false,
+ )
..addOption('config',
abbr: 'c', hide: true, valueHelp: '[file].yaml', help: 'Configuration');
diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart
index 87d5d67..f1e5861 100644
--- a/web_generator/lib/src/interop_gen/transform.dart
+++ b/web_generator/lib/src/interop_gen/transform.dart
@@ -181,8 +181,12 @@
final bool generateAll;
+ final bool strictUnsupported;
+
ProgramMap(this.program, List<String> files,
- {this.filterDeclSet = const [], bool? generateAll})
+ {this.filterDeclSet = const [],
+ bool? generateAll,
+ this.strictUnsupported = false})
: typeChecker = program.getTypeChecker(),
generateAll = generateAll ?? false,
files = p.PathSet.of(files);
@@ -323,7 +327,8 @@
TransformerManager.fromParsedResults(ParserResult result, {Config? config})
: programMap = ProgramMap(result.program, result.files.toList(),
filterDeclSet: config?.includedDeclarations ?? [],
- generateAll: config?.generateAll);
+ generateAll: config?.generateAll,
+ strictUnsupported: config?.strictUnsupported ?? false);
TransformResult transform() {
final outputNodeMap = <String, NodeMap>{};
diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart
index aac412e..87f6ce3 100644
--- a/web_generator/lib/src/interop_gen/transform/transformer.dart
+++ b/web_generator/lib/src/interop_gen/transform/transformer.dart
@@ -86,6 +86,8 @@
bool get generateAll => programMap.generateAll;
+ bool get errorIfUnsupported => programMap.strictUnsupported;
+
Transformer(this.programMap, this._sourceFile,
{Set<String> exportSet = const {}, String? file})
: exportSet = exportSet.map((e) => ExportReference(e, as: e)).toSet(),
@@ -152,7 +154,12 @@
(node.name as TSIdentifier).text != 'global':
return [_transformNamespace(node, namer: namer, parent: parent)];
default:
- throw Exception('Unsupported Declaration Kind: ${node.kind}');
+ if (errorIfUnsupported) {
+ throw Exception('Unsupported Declaration Kind: ${node.kind}');
+ } else {
+ print('WARN: Unsupported Declaration Kind: ${node.kind}');
+ return [];
+ }
}
}
@@ -1460,13 +1467,23 @@
TSSyntaxKind.BigIntKeyword => PrimitiveType.bigint,
TSSyntaxKind.SymbolKeyword => PrimitiveType.symbol,
TSSyntaxKind.NeverKeyword => PrimitiveType.never,
- _ => throw UnsupportedError(
- 'The given type with kind ${type.kind} is not supported yet')
+ _ => null
};
- return BuiltinType.primitiveType(primitiveType,
- shouldEmitJsType: typeArg ? true : null,
- isNullable: primitiveType == PrimitiveType.any ? true : isNullable);
+ if (primitiveType != null) {
+ return BuiltinType.primitiveType(primitiveType,
+ shouldEmitJsType: typeArg ? true : null,
+ isNullable:
+ primitiveType == PrimitiveType.any ? true : isNullable);
+ } else if (errorIfUnsupported) {
+ throw UnsupportedError(
+ 'The given type with kind ${type.kind} is not supported yet');
+ } else {
+ print('WARN: The given type with kind ${type.kind} is '
+ 'not supported yet');
+ return BuiltinType.primitiveType(PrimitiveType.any,
+ isNullable: isNullable);
+ }
}
}
@@ -2160,10 +2177,6 @@
void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) {
switch (decl) {
case final VariableDeclaration v:
- print((
- v.name,
- v.type is TupleType ? (v.type as TupleType).name : null
- ));
filteredDeclarations.add(v.type);
break;
case final CallableDeclaration f:
diff --git a/web_generator/test/assets/unsupported_test.d.ts b/web_generator/test/assets/unsupported_test.d.ts
new file mode 100644
index 0000000..a430bee
--- /dev/null
+++ b/web_generator/test/assets/unsupported_test.d.ts
@@ -0,0 +1,8 @@
+export interface Foo {
+ name: string;
+ length: number;
+ raw: ArrayBuffer;
+}
+export interface Fee {
+ raw: Foo['raw']
+}
\ No newline at end of file
diff --git a/web_generator/test/assets/unsupported_test_expected.dart b/web_generator/test/assets/unsupported_test_expected.dart
new file mode 100644
index 0000000..5d227d0
--- /dev/null
+++ b/web_generator/test/assets/unsupported_test_expected.dart
@@ -0,0 +1,15 @@
+// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:js_interop' as _i1;
+
+extension type Foo._(_i1.JSObject _) implements _i1.JSObject {
+ external String name;
+
+ external double length;
+
+ external _i1.JSArrayBuffer raw;
+}
+extension type Fee._(_i1.JSObject _) implements _i1.JSObject {
+ external _i1.JSAny raw;
+}
diff --git a/web_generator/test/unsupported_test.dart b/web_generator/test/unsupported_test.dart
new file mode 100644
index 0000000..74f3dd2
--- /dev/null
+++ b/web_generator/test/unsupported_test.dart
@@ -0,0 +1,74 @@
+// 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 'dart:io';
+
+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 Unsupported Test', () {
+ final testFile = p.join('test', 'assets', 'unsupported_test.d.ts');
+ final outputFile = p.join('.dart_tool', 'unsupported_test.dart');
+ final expectedFile =
+ p.join('test', 'assets', 'unsupported_test_expected.dart');
+
+ setUpAll(() async {
+ // set up npm
+ await runProc('npm', ['install'], workingDirectory: bindingsGenPath);
+
+ // compile file
+ await compileDartMain(dir: bindingsGenPath);
+ });
+
+ test('Strict Unsupported', () 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',
+ '--strict-unsupported',
+ '--declaration'
+ ],
+ workingDirectory: bindingsGenPath);
+
+ final stderr = await process.stderr.transform(utf8.decoder).toList();
+
+ expect(stderr, isNotEmpty);
+ expect(await process.exitCode, isNot(0));
+ });
+
+ test('Not Strict Unsupported', () async {
+ final inputFilePath = p.relative(testFile, from: bindingsGenPath);
+ final outputFilePath = p.relative(outputFile, from: bindingsGenPath);
+
+ await runProc(
+ 'node',
+ [
+ 'main.mjs',
+ '--input=$inputFilePath',
+ '--output=$outputFilePath',
+ '--declaration'
+ ],
+ workingDirectory: bindingsGenPath);
+
+ // read files
+ final expectedOutput = await File(expectedFile).readAsString();
+ final actualOutput = await File(outputFile).readAsString();
+
+ expect(actualOutput, expectedOutput);
+ });
+ });
+}