Skip to content
Open
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
176 changes: 175 additions & 1 deletion web_generator/lib/src/interop_gen/transform/transformer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ class Transformer {
return [
_transformClassOrInterface(node as TSObjectDeclaration, namer: namer),
];
case TSSyntaxKind.ImportEqualsDeclaration
when (node as TSImportEqualsDeclaration).moduleReference.kind ==
TSSyntaxKind.ExternalModuleReference:
return [
_transformImportEqualsAsNamespace(node, namer: namer, parent: parent),
];
case TSSyntaxKind.ImportEqualsDeclaration
when (node as TSImportEqualsDeclaration).moduleReference.kind !=
TSSyntaxKind.ExternalModuleReference:
Expand Down Expand Up @@ -221,7 +227,6 @@ class Transformer {
}
}

// TODO(): Support `import = require` declarations, https://github.com/dart-lang/web/issues/438
TypeAliasDeclaration _transformImportEqualsDeclarationAsTypeAlias(
TSImportEqualsDeclaration typealias, {
UniqueNamer? namer,
Expand All @@ -248,6 +253,175 @@ class Transformer {
);
}

/// Transforms an import equals declaration with an external module reference
/// (e.g., `import foo = require("bar")`) into a namespace declaration.
/// This normalizes CommonJS-style imports into the same representation as
/// ES6 namespace imports (`import * as foo from "bar"`).
NamespaceDeclaration _transformImportEqualsAsNamespace(
TSImportEqualsDeclaration importEquals, {
UniqueNamer? namer,
NamespaceDeclaration? parent,
}) {
namer ??= this.namer;

final namespaceName = importEquals.name.text;
final moduleRef = importEquals.moduleReference as TSExternalModuleReference;

// Validate that the expression is a string literal
if (moduleRef.expression.kind != TSSyntaxKind.StringLiteral) {
print(
'WARN: Unsupported import = require() with non-string expression: '
'${moduleRef.expression.kind}',
);
// Return empty namespace as fallback
final (:id, name: dartName) = namer.makeUnique(
namespaceName,
'namespace',
);
return NamespaceDeclaration(
name: namespaceName,
dartName: dartName,
id: id,
exported: false,
topLevelDeclarations: {},
namespaceDeclarations: {},
nestableDeclarations: {},
);
Comment on lines +271 to +289
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation logs a warning and returns an empty NamespaceDeclaration when moduleRef.expression.kind is not TSSyntaxKind.StringLiteral. While this prevents a crash, returning an empty namespace might lead to silent failures or unexpected behavior downstream if the generated Dart code relies on a non-empty namespace. Consider throwing an UnsupportedError if errorIfUnsupported is true, or providing a more explicit fallback that clearly indicates the namespace is invalid or empty, rather than silently creating an empty one.

}

// get modifiers
final modifiers = importEquals.modifiers?.toDart ?? [];
final isExported = modifiers.any((m) {
return m.kind == TSSyntaxKind.ExportKeyword;
});

final currentNamespaces = parent != null
? parent.namespaceDeclarations.where((n) => n.name == namespaceName)
: nodeMap.findByName(namespaceName).whereType<NamespaceDeclaration>();

final (name: dartName, :id) = currentNamespaces.isEmpty
? namer.makeUnique(namespaceName, 'namespace')
: (name: null, id: null);

final scopedNamer = ScopedUniqueNamer();

final outputNamespace = currentNamespaces.isNotEmpty
? currentNamespaces.first
: NamespaceDeclaration(
name: namespaceName,
dartName: dartName,
id: id!,
exported: isExported,
topLevelDeclarations: {},
namespaceDeclarations: {},
nestableDeclarations: {},
documentation: _parseAndTransformDocumentation(importEquals),
);
Comment on lines +302 to +319
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The id! operator is used here, but id can be null if currentNamespaces is not empty, as per the preceding ternary operator. This could lead to a runtime Null check operator used on a null value error. When currentNamespaces is not empty, id and dartName should be populated from currentNamespaces.first to ensure they are not null.

    final (name: dartName, id: id) = currentNamespaces.isEmpty
        ? namer.makeUnique(namespaceName, 'namespace')
        : (name: currentNamespaces.first.dartName, id: currentNamespaces.first.id);

    final scopedNamer = ScopedUniqueNamer();

    final outputNamespace = currentNamespaces.isNotEmpty
        ? currentNamespaces.first
        : NamespaceDeclaration(
            name: namespaceName,
            dartName: dartName,
            id: id,
            exported: isExported,
            topLevelDeclarations: {},
            namespaceDeclarations: {},
            nestableDeclarations: {},
            documentation: _parseAndTransformDocumentation(importEquals),
          );


/// Updates the state of the given declaration,
/// allowing cross-references between types and declarations in the
/// namespace, including the namespace itself
void updateNSInParent() {
if (parent != null) {
if (currentNamespaces.isNotEmpty ||
parent.namespaceDeclarations.any((n) => n.name == namespaceName)) {
parent.namespaceDeclarations.remove(currentNamespaces.first);
parent.namespaceDeclarations.add(outputNamespace);
} else {
outputNamespace.parent = parent;
parent.namespaceDeclarations.add(outputNamespace);
}
} else {
nodeMap.update(
outputNamespace.id.toString(),
(v) => outputNamespace,
ifAbsent: () => outputNamespace,
);
}
}
Comment on lines +324 to +341
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The condition currentNamespaces.isNotEmpty || parent.namespaceDeclarations.any((n) => n.name == namespaceName) is problematic. If currentNamespaces is empty, but a matching namespace exists in parent.namespaceDeclarations, the code will attempt to remove(currentNamespaces.first), which will throw an error because currentNamespaces is empty. The logic needs to correctly handle the case where currentNamespaces might be empty but a namespace with the same name exists in the parent's declarations.

    void updateNSInParent() {
      if (parent != null) {
        final existingParentNamespace = parent.namespaceDeclarations.firstWhereOrNull((n) => n.name == namespaceName);
        if (existingParentNamespace != null) {
          parent.namespaceDeclarations.remove(existingParentNamespace);
          parent.namespaceDeclarations.add(outputNamespace);
        } else {
          outputNamespace.parent = parent;
          parent.namespaceDeclarations.add(outputNamespace);
        }
      } else {
        nodeMap.update(
          outputNamespace.id.toString(),
          (v) => outputNamespace,
          ifAbsent: () => outputNamespace,
        );
      }
    }


void transformDeclAndAppendParent(
NamespaceDeclaration outputNamespace,
TSNode decl,
) {
if (outputNamespace.nodes.contains(decl)) return;
if (decl.kind == TSSyntaxKind.EnumMember) {
final tsEnum = (decl as TSEnumMember).parent;
// parse whole enum
final transformedEnum = _transformEnum(tsEnum, namer: namer);

// add enum
if (parent != null) {
parent.nestableDeclarations.add(transformedEnum);
parent.nodes.add(tsEnum);
} else {
nodes.add(tsEnum);
nodeMap.add(transformedEnum);
}

// add all members to namespace
outputNamespace.nodes.addAll(tsEnum.members.toDart);
} else {
final outputDecls = transformAndReturn(
decl,
namer: scopedNamer,
parent: outputNamespace,
);
switch (decl.kind) {
case TSSyntaxKind.ClassDeclaration ||
TSSyntaxKind.InterfaceDeclaration:
final outputDecl = outputDecls.single as TypeDeclaration;
outputDecl.parent = outputNamespace;
outputNamespace.nestableDeclarations.add(outputDecl);
case TSSyntaxKind.EnumDeclaration:
final outputDecl = outputDecls.single as EnumDeclaration;
outputDecl.parent = outputNamespace;
outputNamespace.nestableDeclarations.add(outputDecl);
case TSSyntaxKind.TypeAliasDeclaration:
final outputDecl = outputDecls.single as TypeAliasDeclaration;
outputDecl.parent = outputNamespace;
outputNamespace.nestableDeclarations.add(outputDecl);
default:
outputNamespace.topLevelDeclarations.addAll(outputDecls);
}
outputNamespace.nodes.add(decl);
}

// update namespace state
updateNSInParent();
}

// preload nodemap
updateNSInParent();

// Resolve the symbol at the import name location to get the module's export
final symbol = typeChecker.getSymbolAtLocation(importEquals.name);
final exports = symbol?.exports?.toDart;

if (exports case final exportedMap?) {
for (final symbol in exportedMap.values) {
final decls = symbol.getDeclarations()?.toDart ?? [];
try {
final aliasedSymbol = typeChecker.getAliasedSymbol(symbol);
decls.addAll(aliasedSymbol.getDeclarations()?.toDart ?? []);
} catch (_) {
// throws error if no aliased symbol, so ignore
}
Comment on lines +407 to +409
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching all exceptions with catch (_) can hide underlying issues and make debugging difficult. While the comment suggests it's for ignoring errors when no aliased symbol exists, it's generally better to either check for the existence of an aliased symbol explicitly (if the API allows) or catch a more specific exception type if known. If JSError is the expected exception, consider catching it and checking its message for the specific 'no aliased symbol' case.

        } on JSError catch (e) {
          // Assuming JSError is the specific error thrown when no aliased symbol exists.
          // If other errors can occur, they should be handled differently or rethrown.
          if (!e.message.contains('No aliased symbol')) {
            rethrow;
          }
        }

for (final decl in decls) {
transformDeclAndAppendParent(outputNamespace, decl);
}
}
}

// final update on namespace state
updateNSInParent();

// index names
namer.markUsedSet(scopedNamer);

return outputNamespace;
}

/// Transforms a TS Namespace (identified as a [TSModuleDeclaration] with
/// an identifier name that isn't "global") into a Dart Namespace
/// Representation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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;

import 'package:meta/meta.dart' as _i2;

@_i1.JS()
external Vector2D get v2d;
@_i1.JS()
external Vector3D get v3d;
extension type Vector2D._(_i1.JSObject _) implements Vector {
external Vector2D(num x, num y);

external double x;

external double y;

external Vector2D unit();
@_i2.redeclare
external double get magnitude;
@_i2.redeclare
external double get directionAngle;
external Point2D moveFrom(Point2D point);
external static Vector2D from(num magnitude, num at);
external static Vector2D fromPoints(Point2D start, Point2D end);
}
extension type Point2D._(_i1.JSObject _) implements _i1.JSObject {
external double x;

external double y;
}
extension type Vector._(_i1.JSObject _) implements _i1.JSObject {
external double get magnitude;
external double get directionAngle;
}
extension type Vector3D._(_i1.JSObject _) implements Vector {
external Vector3D(num x, num y, num z);

external double x;

external double y;

external double z;

external Vector3D unit();
@_i2.redeclare
external double get magnitude;
external DirectionAngles get directionAngles;
@_i2.redeclare
external double get directionAngle;
external Point3D moveFrom(Point3D point);
external static Vector3D from(num magnitude, DirectionAngles at);
external static Vector3D fromPoints(Point3D start, Point3D end);
}
extension type DirectionAngles._(_i1.JSObject _) implements _i1.JSObject {
external double alpha;

external double beta;

external double gamma;
}
extension type Point3D._(_i1.JSObject _) implements _i1.JSObject {
external double x;

external double y;

external double z;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Test file for import = require() support
// Tests that import = require() declarations are properly transformed
// into namespace imports

import Vector = require("./classes_input");

// Declare a variable using the imported namespace
// The Vector namespace should contain Vector2D, Vector3D, etc.
export declare const v2d: Vector.Vector2D;
export declare const v3d: Vector.Vector3D;