Skip to content

Commit 5d4fd3c

Browse files
authored
refactor(js_interop_gen): extract pure utilities and data classes (#545)
Cleans up transformer.dart by moving isolated logic and pure utility functions that do not rely on the core transformer state. - Extracted `ExportReference` data class into a dedicated `export_reference.dart` file. - Extracted context-independent helper functions (`parseModifiers`, `parseQualifiedNameParts`, `parseQualifiedNameFromTSQualifiedName`, `parseQualifiedName`, and `toCamelCase`) into a new `utils.dart` file. - Inlined the short `parseNumericLiteral` and `parseStringLiteral` helpers directly at their call sites in the `Transformer` class. - Updated imports, made the extracted utility functions public, and cleaned up unused imports. No functional changes: All tests pass, and package:web code generation yields a zero diff.
1 parent 294391c commit 5d4fd3c

4 files changed

Lines changed: 161 additions & 124 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
class ExportReference {
6+
final String name;
7+
final String as;
8+
final bool defaultExport;
9+
10+
const ExportReference(
11+
this.name, {
12+
required this.as,
13+
this.defaultExport = false,
14+
});
15+
16+
@override
17+
bool operator ==(Object other) =>
18+
other is ExportReference &&
19+
name == other.name &&
20+
as == other.as &&
21+
defaultExport == other.defaultExport;
22+
23+
@override
24+
int get hashCode => Object.hash(name, as, defaultExport);
25+
}

js_interop_gen/lib/src/interop_gen/transform/transformer.dart

Lines changed: 11 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'dart:collection';
65
import 'dart:js_interop';
76

87
import 'package:collection/collection.dart';
@@ -26,28 +25,8 @@ import '../hasher.dart';
2625
import '../namer.dart';
2726
import '../qualified_name.dart';
2827
import '../transform.dart';
29-
30-
class ExportReference {
31-
final String name;
32-
final String as;
33-
final bool defaultExport;
34-
35-
const ExportReference(
36-
this.name, {
37-
required this.as,
38-
this.defaultExport = false,
39-
});
40-
41-
@override
42-
bool operator ==(Object other) =>
43-
other is ExportReference &&
44-
name == other.name &&
45-
as == other.as &&
46-
defaultExport == other.defaultExport;
47-
48-
@override
49-
int get hashCode => Object.hash(name, as, defaultExport);
50-
}
28+
import 'export_reference.dart';
29+
import 'utils.dart';
5130

5231
/// A class for transforming nodes in a given [file]
5332
///
@@ -603,14 +582,12 @@ class Transformer {
603582
? (nameNode as TSIdentifier).text
604583
: (nameNode as TSLiteralExpression).text;
605584
final nameForDart = nameNode.kind == TSSyntaxKind.StringLiteral
606-
? dartRename(_toCamelCase(name))
585+
? dartRename(toCamelCase(name))
607586
: name;
608587

609588
final (:id, name: dartName) = parentNamer.makeUnique(nameForDart, 'var');
610589

611-
final (:isStatic, :isReadonly, :scope) = _parseModifiers(
612-
property.modifiers,
613-
);
590+
final (:isStatic, :isReadonly, :scope) = parseModifiers(property.modifiers);
614591

615592
ReferredType? propType;
616593
if (property.type case final type? when ts.isTypeReferenceNode(type)) {
@@ -664,9 +641,7 @@ class Transformer {
664641

665642
final typeParams = method.typeParameters?.toDart;
666643

667-
final (:isStatic, isReadonly: _, :scope) = _parseModifiers(
668-
method.modifiers,
669-
);
644+
final (:isStatic, isReadonly: _, :scope) = parseModifiers(method.modifiers);
670645

671646
ReferredType? methodType;
672647
if (method.type case final type? when ts.isTypeReferenceNode(type)) {
@@ -749,7 +724,7 @@ class Transformer {
749724
final (isStatic: _, isReadonly: _, :scope) =
750725
(constructor.isA<TSConstructorDeclaration>() ||
751726
constructor.kind == TSSyntaxKind.Constructor)
752-
? _parseModifiers((constructor as TSConstructorDeclaration).modifiers)
727+
? parseModifiers((constructor as TSConstructorDeclaration).modifiers)
753728
: (isStatic: false, isReadonly: false, scope: DeclScope.public);
754729

755730
return ConstructorDeclaration(
@@ -824,7 +799,7 @@ class Transformer {
824799

825800
final typeParams = indexSignature.typeParameters?.toDart;
826801

827-
final (:isStatic, :isReadonly, :scope) = _parseModifiers(
802+
final (:isStatic, :isReadonly, :scope) = parseModifiers(
828803
indexSignature.modifiers,
829804
);
830805

@@ -898,7 +873,7 @@ class Transformer {
898873

899874
final typeParams = getter.typeParameters?.toDart;
900875

901-
final (isStatic: _, isReadonly: _, :scope) = _parseModifiers(
876+
final (isStatic: _, isReadonly: _, :scope) = parseModifiers(
902877
getter.modifiers,
903878
);
904879

@@ -955,7 +930,7 @@ class Transformer {
955930

956931
final typeParams = setter.typeParameters?.toDart;
957932

958-
final (isStatic: _, isReadonly: _, :scope) = _parseModifiers(
933+
final (isStatic: _, isReadonly: _, :scope) = parseModifiers(
959934
setter.modifiers,
960935
);
961936

@@ -1119,9 +1094,7 @@ class Transformer {
11191094
switch (memInitializer.kind) {
11201095
case TSSyntaxKind.NumericLiteral:
11211096
// parse numeric literal
1122-
final value = _parseNumericLiteral(
1123-
memInitializer as TSNumericLiteral,
1124-
);
1097+
final value = num.parse((memInitializer as TSNumericLiteral).text);
11251098
final primitiveType = value is int
11261099
? PrimitiveType.int
11271100
: PrimitiveType.double;
@@ -1145,9 +1118,7 @@ class Transformer {
11451118
break;
11461119
case TSSyntaxKind.StringLiteral:
11471120
// parse string literal
1148-
final value = _parseStringLiteral(
1149-
memInitializer as TSStringLiteral,
1150-
);
1121+
final value = (memInitializer as TSStringLiteral).text;
11511122
const primitiveType = PrimitiveType.string;
11521123
members.add(
11531124
EnumMember(
@@ -1194,14 +1165,6 @@ class Transformer {
11941165
);
11951166
}
11961167

1197-
num _parseNumericLiteral(TSNumericLiteral numericLiteral) {
1198-
return num.parse(numericLiteral.text);
1199-
}
1200-
1201-
String _parseStringLiteral(TSStringLiteral stringLiteral) {
1202-
return stringLiteral.text;
1203-
}
1204-
12051168
TypeAliasDeclaration _transformTypeAlias(
12061169
TSTypeAliasDeclaration typealias, {
12071170
UniqueNamer? namer,
@@ -3109,79 +3072,3 @@ class Transformer {
31093072
return filteredDeclarations;
31103073
}
31113074
}
3112-
3113-
({bool isReadonly, bool isStatic, DeclScope scope}) _parseModifiers([
3114-
TSNodeArray<TSNode>? modifiers,
3115-
]) {
3116-
var isReadonly = false;
3117-
var isStatic = false;
3118-
var scope = DeclScope.public;
3119-
3120-
for (final modifier in modifiers?.toDart ?? <TSNode>[]) {
3121-
switch (modifier.kind) {
3122-
case TSSyntaxKind.StaticKeyword:
3123-
isStatic = true;
3124-
break;
3125-
case TSSyntaxKind.ReadonlyKeyword:
3126-
isReadonly = true;
3127-
break;
3128-
case TSSyntaxKind.PrivateKeyword:
3129-
scope = DeclScope.private;
3130-
break;
3131-
case TSSyntaxKind.ProtectedKeyword:
3132-
scope = DeclScope.protected;
3133-
break;
3134-
case TSSyntaxKind.PublicKeyword:
3135-
scope = DeclScope.public;
3136-
break;
3137-
default:
3138-
break;
3139-
}
3140-
}
3141-
3142-
return (isStatic: isStatic, isReadonly: isReadonly, scope: scope);
3143-
}
3144-
3145-
Iterable<QualifiedNamePart> _parseQualifiedName(TSQualifiedName name) {
3146-
final list = <QualifiedNamePart>[];
3147-
if (name.left.kind == TSSyntaxKind.Identifier) {
3148-
list.add(QualifiedNamePart((name.left as TSIdentifier).text));
3149-
} else {
3150-
list.addAll(_parseQualifiedName(name.left as TSQualifiedName));
3151-
}
3152-
3153-
list.add(QualifiedNamePart(name.right.text));
3154-
3155-
return list;
3156-
}
3157-
3158-
QualifiedName parseQualifiedNameFromTSQualifiedName(TSQualifiedName name) {
3159-
final list = LinkedList<QualifiedNamePart>();
3160-
list.addAll(_parseQualifiedName(name));
3161-
return QualifiedName(list);
3162-
}
3163-
3164-
QualifiedName parseQualifiedName(
3165-
@UnionOf([TSQualifiedName, TSIdentifier]) TSNode name,
3166-
) {
3167-
if (name.kind == TSSyntaxKind.Identifier) {
3168-
return QualifiedName.raw((name as TSIdentifier).text);
3169-
} else {
3170-
return parseQualifiedNameFromTSQualifiedName(name as TSQualifiedName);
3171-
}
3172-
}
3173-
3174-
String _toCamelCase(String text) {
3175-
final parts = text.split(RegExp(r'[-=]'));
3176-
final sb = StringBuffer();
3177-
for (var i = 0; i < parts.length; i++) {
3178-
final part = parts[i];
3179-
if (part.isEmpty) continue;
3180-
if (i == 0) {
3181-
sb.write(part.substring(0, 1).toLowerCase() + part.substring(1));
3182-
} else {
3183-
sb.write(part.substring(0, 1).toUpperCase() + part.substring(1));
3184-
}
3185-
}
3186-
return sb.toString();
3187-
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
import 'dart:js_interop';
7+
8+
import '../../ast/base.dart';
9+
import '../../js/annotations.dart';
10+
import '../../js/typescript.types.dart';
11+
import '../qualified_name.dart';
12+
13+
({bool isReadonly, bool isStatic, DeclScope scope}) parseModifiers([
14+
TSNodeArray<TSNode>? modifiers,
15+
]) {
16+
var isReadonly = false;
17+
var isStatic = false;
18+
var scope = DeclScope.public;
19+
20+
for (final modifier in modifiers?.toDart ?? <TSNode>[]) {
21+
switch (modifier.kind) {
22+
case TSSyntaxKind.StaticKeyword:
23+
isStatic = true;
24+
break;
25+
case TSSyntaxKind.ReadonlyKeyword:
26+
isReadonly = true;
27+
break;
28+
case TSSyntaxKind.PrivateKeyword:
29+
scope = DeclScope.private;
30+
break;
31+
case TSSyntaxKind.ProtectedKeyword:
32+
scope = DeclScope.protected;
33+
break;
34+
case TSSyntaxKind.PublicKeyword:
35+
scope = DeclScope.public;
36+
break;
37+
default:
38+
break;
39+
}
40+
}
41+
42+
return (isStatic: isStatic, isReadonly: isReadonly, scope: scope);
43+
}
44+
45+
Iterable<QualifiedNamePart> parseQualifiedNameParts(TSQualifiedName name) {
46+
final list = <QualifiedNamePart>[];
47+
if (name.left.kind == TSSyntaxKind.Identifier) {
48+
list.add(QualifiedNamePart((name.left as TSIdentifier).text));
49+
} else {
50+
list.addAll(parseQualifiedNameParts(name.left as TSQualifiedName));
51+
}
52+
53+
list.add(QualifiedNamePart(name.right.text));
54+
55+
return list;
56+
}
57+
58+
QualifiedName parseQualifiedNameFromTSQualifiedName(TSQualifiedName name) {
59+
final list = LinkedList<QualifiedNamePart>()
60+
..addAll(parseQualifiedNameParts(name));
61+
return QualifiedName(list);
62+
}
63+
64+
QualifiedName parseQualifiedName(
65+
@UnionOf([TSQualifiedName, TSIdentifier]) TSNode name,
66+
) {
67+
if (name.kind == TSSyntaxKind.Identifier) {
68+
return QualifiedName.raw((name as TSIdentifier).text);
69+
} else {
70+
return parseQualifiedNameFromTSQualifiedName(name as TSQualifiedName);
71+
}
72+
}
73+
74+
String toCamelCase(String text) {
75+
final parts = text.split(RegExp(r'[-=]'));
76+
final sb = StringBuffer();
77+
var first = true;
78+
for (final part in parts) {
79+
if (part.isEmpty) continue;
80+
if (first) {
81+
sb.write(part[0].toLowerCase());
82+
sb.write(part.substring(1));
83+
first = false;
84+
} else {
85+
sb.write(part[0].toUpperCase());
86+
sb.write(part.substring(1));
87+
}
88+
}
89+
return sb.toString();
90+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('node')
6+
library;
7+
8+
import 'package:js_interop_gen/src/interop_gen/transform/utils.dart';
9+
import 'package:test/test.dart';
10+
11+
void main() {
12+
group('toCamelCase tests', () {
13+
final testCases = {
14+
'webkit-appearance': 'webkitAppearance',
15+
'-webkit-appearance': 'webkitAppearance',
16+
'accept-charset': 'acceptCharset',
17+
'accept-=charset': 'acceptCharset',
18+
'accept': 'accept',
19+
'-accept': 'accept',
20+
'': '',
21+
'-': '',
22+
'--': '',
23+
'a-b': 'aB',
24+
'-a-b': 'aB',
25+
'Webkit-Appearance': 'webkitAppearance',
26+
'accept-charset-': 'acceptCharset',
27+
};
28+
29+
for (final MapEntry(key: input, value: expected) in testCases.entries) {
30+
test('"$input" -> "$expected"', () {
31+
expect(toCamelCase(input), equals(expected));
32+
});
33+
}
34+
});
35+
}

0 commit comments

Comments
 (0)