Skip to content

Commit 951961e

Browse files
[interop] Refactoring Generator to make use of configuration options (#408)
* Added function var args config * added config test case * Completed Configuration Integration * analysis issue * resolved some issues with config * used specific subtypes * fixed minor analysis
1 parent 354e229 commit 951961e

File tree

10 files changed

+400
-42
lines changed

10 files changed

+400
-42
lines changed

web_generator/lib/src/config.dart

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:dart_style/dart_style.dart';
6+
import 'package:path/path.dart' as p;
67
import 'package:pub_semver/pub_semver.dart';
78
import 'package:yaml/yaml.dart';
89

10+
class FunctionConfig {
11+
/// The number of variable arguments
12+
final int? varArgs;
13+
14+
const FunctionConfig({this.varArgs});
15+
}
16+
917
abstract interface class Config {
1018
/// The name for the configuration
1119
String? get name;
@@ -28,13 +36,23 @@ abstract interface class Config {
2836
/// The Dart Language Version to use
2937
Version get languageVersion;
3038

39+
/// Function configuration
40+
FunctionConfig? get functions;
41+
3142
bool get singleFileOutput => input.length == 1;
3243

33-
factory Config({
34-
required List<String> input,
35-
required String output,
36-
required Version languageVersion,
37-
}) = ConfigImpl._;
44+
/// Include the following declarations when generating JS interop code
45+
///
46+
/// This could be a plain name for a declaration, or a [RegExp] pattern
47+
/// If empty, all declarations will be generated by default
48+
List<String> get includedDeclarations;
49+
50+
factory Config(
51+
{required List<String> input,
52+
required String output,
53+
required Version languageVersion,
54+
FunctionConfig? functions,
55+
List<String> includedDeclarations}) = ConfigImpl._;
3856
}
3957

4058
class ConfigImpl implements Config {
@@ -59,14 +77,20 @@ class ConfigImpl implements Config {
5977
@override
6078
String? preamble;
6179

62-
ConfigImpl._({
63-
required this.input,
64-
required this.output,
65-
required this.languageVersion,
66-
});
80+
@override
81+
FunctionConfig? functions;
82+
83+
@override
84+
List<String> includedDeclarations;
85+
86+
ConfigImpl._(
87+
{required this.input,
88+
required this.output,
89+
required this.languageVersion,
90+
this.functions,
91+
this.includedDeclarations = const []});
6792

6893
@override
69-
// TODO: implement singleFileOutput
7094
bool get singleFileOutput => input.length == 1;
7195
}
7296

@@ -95,36 +119,58 @@ class YamlConfig implements Config {
95119
@override
96120
String? preamble;
97121

122+
@override
123+
FunctionConfig? functions;
124+
125+
@override
126+
List<String> includedDeclarations;
127+
98128
YamlConfig._(
99129
{required this.filename,
100130
required this.input,
101131
required this.output,
102132
this.description,
103133
this.name,
104134
this.preamble,
135+
this.functions,
136+
this.includedDeclarations = const [],
105137
String? languageVersion})
106138
: languageVersion = languageVersion == null
107139
? DartFormatter.latestLanguageVersion
108140
: Version.parse(languageVersion);
109141

110-
factory YamlConfig.fromYaml(YamlMap yaml, {required String filename}) {
111-
List<String> input;
142+
factory YamlConfig.fromYaml(YamlMap yaml,
143+
{required String filename, List<String>? input, String? output}) {
144+
List<String> inputFiles;
112145
final yamlInput = yaml['input'];
113146
if (yamlInput is YamlList) {
114-
input = yamlInput.map((y) => y is String ? y : y.toString()).toList();
147+
inputFiles =
148+
yamlInput.map((y) => y is String ? y : y.toString()).toList();
115149
} else if (yamlInput is String) {
116-
input = [yamlInput];
150+
inputFiles = [yamlInput];
151+
} else if (input != null) {
152+
inputFiles = input;
117153
} else {
118154
throw TypeError();
119155
}
120156

121157
return YamlConfig._(
122158
filename: Uri.file(filename),
123-
input: input,
124-
output: yaml['output'] as String,
159+
input: inputFiles
160+
.map((file) => p.join(p.dirname(filename), file))
161+
.toList(),
162+
output:
163+
p.join(p.dirname(filename), (yaml['output'] ?? output) as String),
125164
name: yaml['name'] as String?,
126165
description: yaml['description'] as String?,
127166
languageVersion: yaml['language_version'] as String?,
128-
preamble: yaml['preamble'] as String?);
167+
preamble: yaml['preamble'] as String?,
168+
// TODO: Can we consider using `json_serializable`?
169+
functions: FunctionConfig(
170+
varArgs: (yaml['functions'] as YamlMap?)?['varargs'] as int?),
171+
includedDeclarations: (yaml['include'] as YamlList?)
172+
?.map<String>((node) => node.toString())
173+
.toList() ??
174+
[]);
129175
}
130176
}

web_generator/lib/src/dart_main.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ void main(List<String> args) async {
4848
final configContent = fs.readFileSync(
4949
filename.toJS, JSReadFileOptions(encoding: 'utf8'.toJS)) as JSString;
5050
final yaml = loadYamlDocument(configContent.toDart);
51-
config =
52-
YamlConfig.fromYaml(yaml.contents as YamlMap, filename: filename);
51+
config = YamlConfig.fromYaml(
52+
yaml.contents as YamlMap,
53+
filename: filename,
54+
);
5355
} else {
5456
config = Config(
5557
input: argResult['input'] as List<String>,
@@ -67,19 +69,23 @@ Future<void> generateJSInteropBindings(Config config) async {
6769
final jsDeclarations = parseDeclarationFiles(config.input);
6870

6971
// transform declarations
70-
final dartDeclarations = transform(jsDeclarations);
72+
final dartDeclarations = transform(jsDeclarations, config: config);
7173

7274
// generate
73-
final generatedCodeMap = dartDeclarations.generate();
75+
final generatedCodeMap = dartDeclarations.generate(config);
76+
77+
final configOutput = config.output;
78+
79+
if (generatedCodeMap.isEmpty) return;
7480

7581
// write code to file
7682
if (generatedCodeMap.length == 1) {
7783
final entry = generatedCodeMap.entries.first;
78-
fs.writeFileSync(config.output.toJS, entry.value.toJS);
84+
fs.writeFileSync(configOutput.toJS, entry.value.toJS);
7985
} else {
8086
for (final entry in generatedCodeMap.entries) {
8187
fs.writeFileSync(
82-
p.join(config.output, p.basename(entry.key)).toJS, entry.value.toJS);
88+
p.join(configOutput, p.basename(entry.key)).toJS, entry.value.toJS);
8389
}
8490
}
8591
}

web_generator/lib/src/interop_gen/transform.dart

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,59 @@
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:convert';
56
import 'dart:js_interop';
67

78
import 'package:code_builder/code_builder.dart';
89
import 'package:dart_style/dart_style.dart';
910

1011
import '../ast/base.dart';
12+
import '../config.dart';
1113
import '../js/typescript.dart' as ts;
1214
import '../js/typescript.types.dart';
1315
import 'namer.dart';
1416
import 'parser.dart';
1517
import 'transform/transformer.dart';
1618

19+
void _setGlobalOptions(Config config) {
20+
GlobalOptions.variardicArgsCount = config.functions?.varArgs ?? 4;
21+
}
22+
1723
class TransformResult {
1824
ProgramDeclarationMap programMap;
1925

2026
TransformResult._(this.programMap);
2127

2228
// TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads
2329
// (namespaces + functions, multiple interfaces, etc)
24-
Map<String, String> generate() {
30+
Map<String, String> generate(Config config) {
2531
final emitter = DartEmitter.scoped(useNullSafetySyntax: true);
2632
final formatter = DartFormatter(
2733
languageVersion: DartFormatter.latestShortStyleLanguageVersion);
34+
35+
_setGlobalOptions(config);
36+
2837
return programMap.map((file, declMap) {
2938
final specs = declMap.decls.values.map((d) {
3039
return switch (d) {
3140
final Declaration n => n.emit(),
3241
final Type _ => null,
3342
};
3443
}).whereType<Spec>();
35-
final lib = Library((l) => l
36-
..ignoreForFile.addAll(
37-
['constant_identifier_names', 'non_constant_identifier_names'])
38-
..body.addAll(specs));
44+
final lib = Library((l) {
45+
if (config.preamble case final preamble?) {
46+
l.comments.addAll(const LineSplitter().convert(preamble).map((l) {
47+
if (l.startsWith('//')) {
48+
return l.replaceFirst(RegExp(r'^\/\/\s*'), '');
49+
}
50+
return l;
51+
}));
52+
}
53+
l
54+
..ignoreForFile.addAll(
55+
['constant_identifier_names', 'non_constant_identifier_names'])
56+
..body.addAll(specs);
57+
});
3958
return MapEntry(
4059
file,
4160
formatter.format('${lib.accept(emitter)}'
@@ -60,26 +79,31 @@ extension type NodeMap._(Map<String, Node> decls) implements Map<String, Node> {
6079

6180
typedef ProgramDeclarationMap = Map<String, NodeMap>;
6281

63-
TransformResult transform(ParserResult parsedDeclarations) {
82+
TransformResult transform(ParserResult parsedDeclarations,
83+
{required Config config}) {
6484
final programDeclarationMap = <String, NodeMap>{};
6585

6686
for (final file in parsedDeclarations.files) {
6787
if (programDeclarationMap.containsKey(file)) continue;
6888

69-
transformFile(parsedDeclarations.program, file, programDeclarationMap);
89+
transformFile(parsedDeclarations.program, file, programDeclarationMap,
90+
config: config);
7091
}
7192

7293
return TransformResult._(programDeclarationMap);
7394
}
7495

7596
void transformFile(ts.TSProgram program, String file,
76-
Map<String, NodeMap> programDeclarationMap) {
97+
Map<String, NodeMap> programDeclarationMap,
98+
{required Config config}) {
7799
final src = program.getSourceFile(file);
100+
78101
if (src == null) return;
79102

80103
final typeChecker = program.getTypeChecker();
81104

82-
final transformer = Transformer(programDeclarationMap, typeChecker);
105+
final transformer = Transformer(programDeclarationMap, typeChecker,
106+
filterDeclSet: config.includedDeclarations);
83107

84108
ts.forEachChild(
85109
src,

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,28 @@ class Transformer {
2323
/// The type checker for the given program
2424
final ts.TSTypeChecker typeChecker;
2525

26-
/// A set of declarations to export
26+
/// A set of declarations to export updated during transformation
2727
final Set<String> exportSet;
2828

29+
/// A set of declarations to filter for
30+
final List<String> filterDeclSet;
31+
32+
/// The declarations as globs
33+
List<RegExp> get filterDeclSetPatterns => filterDeclSet.map((decl) {
34+
final escapedDecl = RegExp.escape(decl);
35+
if (escapedDecl == decl) return RegExp('^$decl\$');
36+
return RegExp(decl);
37+
}).toList();
38+
2939
/// namer, for giving elements unique names
3040
final UniqueNamer namer;
3141

3242
final ProgramDeclarationMap programMap;
3343

3444
Transformer(this.programMap, this.typeChecker,
35-
{Iterable<String> exportSet = const []})
45+
{Set<String> exportSet = const {}, List<String> filterDeclSet = const []})
3646
: exportSet = exportSet.toSet(),
47+
filterDeclSet = filterDeclSet.toList(),
3748
namer = UniqueNamer();
3849

3950
void transform(TSNode node) {
@@ -433,14 +444,13 @@ class Transformer {
433444

434445
// filter out for export declarations
435446
nodeMap.forEach((id, node) {
436-
if (exportSet.contains(node.name)) {
437-
filteredDeclarations[id] = node;
438-
}
439-
440447
// get decls with `export` keyword
441448
switch (node) {
442449
case final ExportableDeclaration e:
443-
if (e.exported) {
450+
if (e.exported &&
451+
(filterDeclSet.isEmpty ||
452+
filterDeclSetPatterns
453+
.any((pattern) => pattern.hasMatch(e.name)))) {
444454
filteredDeclarations.add(e);
445455
}
446456
break;

web_generator/test/assets/config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Interop Config Test
2+
preamble: |
3+
// This preamble is for testing only.
4+
// GENERATED FILE: DO NOT EDIT
5+
//
6+
// Created by `web_generator`
7+
input: test.d.ts
8+
output: '../../.dart_tool/test_config.dart'
9+
include:
10+
- APP_NAME
11+
- "(.*)User" # Glob
12+
- "user*" # Glob
13+
- currentUserCount
14+
- toggleAdminMode
15+
- systemStatus
16+
- changeRoleOfSetOfUsers
17+
functions:
18+
varargs: 8

web_generator/test/assets/test.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
declare enum UserRole {
2+
Guest = 0,
3+
Viewer = 1,
4+
Editor = 2,
5+
Administrator = 3
6+
}
7+
declare enum HttpStatusCode {
8+
OK = "200 OK",
9+
CREATED = "201 Created",
10+
BAD_REQUEST = "400 Bad Request",
11+
UNAUTHORIZED = "401 Unauthorized",
12+
NOT_FOUND = "404 Not Found",
13+
INTERNAL_SERVER_ERROR = "500 Internal Server Error"
14+
}
15+
export declare const APP_NAME: string;
16+
export declare const APP_NAMES: string;
17+
declare let currentUserCount: number;
18+
declare let isAdminMode: boolean;
19+
declare let systemStatus: string;
20+
type User = string;
21+
export declare function initializeApp(): void;
22+
export declare function loginUser(username: string, role: UserRole): User;
23+
export declare function changeRoleOfUser(user: User, newRole: UserRole): User;
24+
export declare function changeRoleOfUsers(newRole: UserRole, ...users: User[]): User[]; /* Once we add maps, make this a map */
25+
export declare function changeRoleOfSetOfUsers(newRole: UserRole, ...users: User[]): User[];
26+
export declare function logoutUser(user: User): void;
27+
export declare function changeSystemStatus(newStatus: string): void;
28+
export declare function toggleAdminMode(): void;
29+
export declare function processHttpRequest(statusCode: HttpStatusCode): void;
30+
export declare const user1: User;
31+
export declare const user2: User;
32+
export declare const adminUser: User;

0 commit comments

Comments
 (0)