Skip to content

Commit a49a24d

Browse files
committed
Fixed bug in SyntheticBuilder.
1 parent d2de292 commit a49a24d

File tree

4 files changed

+152
-57
lines changed

4 files changed

+152
-57
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11

2+
## 0.3.0
3+
* Updated dependencies.
4+
* Requires SDK ^0.3.9 and analyzer ^8.2.0
5+
* Fixed bug in SyntheticBuilder related to the sorting process order of
6+
dependent input files.
7+
28
## 0.2.8
39
* Updated dependencies
410
* Requires SDK ^0.3.6, and analyzer ^7.0.0.

README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,43 @@
55

66
## Introduction
77

8-
Source code generation has become an important software development tool when building and maintaining a large number of data models, data access object, widgets, etc.
8+
Source code generation has become an important software development tool
9+
when building and maintaining a large number of data models,
10+
data access object, widgets, etc.
911

1012
The premise of *source code generation* is that we can specify
11-
(hopefully few) details and flesh out the rest of the classes, and methods during the build process.
13+
(hopefully few) details and flesh out the rest of the classes,
14+
and methods during the build process.
1215

13-
The build process consists of scannig the appropriate files, extracting the required information,
14-
generating the source code, and writing the source code to certain files. The build process
15-
also entails keeping track of files changes, delete conflicting files, reporting issues and progress, etc.
16+
The build process consists of scannig the appropriate files,
17+
extracting the required information,
18+
generating the source code, and writing the source code to certain files.
19+
The build process
20+
also entails keeping track of files changes,
21+
delete conflicting files, reporting issues and progress.
1622

1723
Source code generation using Dart relies heavily on *constants* known at compile time.
1824
Dart's static [`analyzer`][analyzer] provides access to libraries, classes,
1925
class fields, class methods, functions, variables, etc in the form of [`Elements`][Elements].
20-
Compile-time constant expressions are represented by a [`DartObject`][DartObject] and can be accessed by using [`computeConstantValue()`][computeConstantValue()] a method available for elements representing a variable.
26+
Compile-time constant expressions are represented by a [`DartObject`][DartObject]
27+
and can be accessed by using [`computeConstantValue()`][computeConstantValue()] a method available for elements representing a variable.
2128

22-
In practice, we mark constant constant classes with annotations and instruct the builder to processes only
23-
the annotated objects.
29+
In practice, we mark constant constant classes with annotations and instruct
30+
the builder to processes only the annotated objects.
2431

2532

2633
The library [`merging_builder`][merging_builder] includes the following (synthetic input) builder classes:
2734

28-
* [`MergingBuilder`][class-merging-builder] reads **several input files** and writes merged output to **one output file**. The builder provides the option to sort the processing order of the input files in reverse topological order.
35+
* [`MergingBuilder`][class-merging-builder] reads **several input files** and writes
36+
merged output to **one output file**. The builder provides the option to
37+
sort the processing order of the input files in reverse topological order.
38+
(In the file `build.yaml`, under builder `options` set: `sort_assets: true`).
2939

30-
* [`StandaloneBuilder`][StandaloneBuilder] reads one or several input files and writes standalone files to a custom location. In this context, **standalone** means the output files may be written to a **custom folder** and not only the **extension** but the **name** of the output file can be configured (as opposed to using part files).
40+
* [`StandaloneBuilder`][StandaloneBuilder] reads one or several input files and
41+
writes standalone files to a custom location. In this context, **standalone**
42+
means the output files may be written to a **custom folder** and
43+
not only the **extension** but the **name** of the output file can
44+
be configured (as opposed to using part files).
3145

3246

3347
## Usage

lib/src/builders/synthetic_builder.dart

Lines changed: 121 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:collection';
2+
13
import 'package:build/build.dart';
24
import 'package:dart_style/dart_style.dart';
35
import 'package:directed_graph/directed_graph.dart';
@@ -101,6 +103,49 @@ abstract class SyntheticBuilder<S extends SyntheticInput> implements Builder {
101103
return result;
102104
}
103105

106+
/// Recursively adds an [AssetId] representing a library to a graph.
107+
/// If the library imports other
108+
/// libraries then the respective asset ids will be added as graph edges.
109+
///
110+
/// ---
111+
/// Note: The graph is acyclic only if no library imports itself (indirectly).
112+
Future<void> _addAssetVertex({
113+
required DirectedGraph<AssetId> assetGraph,
114+
required AssetId assetId,
115+
required Set<AssetId> scannedAssetIds,
116+
required BuildStep buildStep,
117+
}) async {
118+
scannedAssetIds.add(assetId);
119+
final library = await buildStep.resolver.libraryFor(assetId);
120+
for (final fragment in library.fragments) {
121+
for (final importedLibrary in fragment.importedLibraries) {
122+
final uri = importedLibrary.uri;
123+
switch (uri.scheme) {
124+
case 'package' || 'asset':
125+
final importedAssetId = AssetId.resolve(uri, from: assetId);
126+
assetGraph.addEdges(assetId, {importedAssetId});
127+
// Recursive call. Check if assetId exists!
128+
if (!scannedAssetIds.contains(importedAssetId)) {
129+
// log.fine('recursive call: $importedAssetId');
130+
await _addAssetVertex(
131+
assetGraph: assetGraph,
132+
assetId: importedAssetId,
133+
buildStep: buildStep,
134+
scannedAssetIds: scannedAssetIds,
135+
);
136+
}
137+
break;
138+
default:
139+
// log.finer(
140+
// 'Info: In \'SyntheticBuilder\' could not resolve '
141+
// 'library ${importedLibrary.displayName} '
142+
// 'with uri.scheme: ${uri.scheme}.',
143+
// );
144+
}
145+
}
146+
}
147+
}
148+
104149
/// Returns an ordered set of library asset ids ordered in reverse topological
105150
/// dependency order.
106151
/// * If a file B includes a file A, then A will be appear
@@ -109,64 +154,94 @@ abstract class SyntheticBuilder<S extends SyntheticInput> implements Builder {
109154
Future<Set<AssetId>> orderedLibraryAssetIds(BuildStep buildStep) async {
110155
final assetGraph = DirectedGraph<AssetId>(
111156
{},
112-
comparator: ((v1, v2) => -v1.compareTo(v2)),
157+
// Alphabetic order
158+
comparator: ((v1, v2) => v1.compareTo(v2)),
113159
);
114160

115-
// An assetId map of all input libraries with the uri as key.
116-
final assetMap = <Uri, AssetId>{};
161+
final scannedAssetIds = <AssetId>{};
162+
163+
/// The assetIds representing the libraries that will be processed by the
164+
/// builder.
165+
final assetIds = <AssetId>{};
117166

118167
// Access libraries
119-
await for (final input in buildStep.findAssets(Glob(inputFiles))) {
168+
await for (final assetId in buildStep.findAssets(Glob(inputFiles))) {
120169
// Check if input file is a library.
121-
if (await buildStep.resolver.isLibrary(input)) {
122-
assetMap[input.uri] = input;
123-
assetGraph.addEdges(assetMap[input.uri]!, {});
170+
if (await buildStep.resolver.isLibrary(assetId)) {
171+
await _addAssetVertex(
172+
assetGraph: assetGraph,
173+
assetId: assetId,
174+
buildStep: buildStep,
175+
scannedAssetIds: scannedAssetIds,
176+
);
177+
assetIds.add(assetId);
124178
}
125179
}
126180

127-
for (final assetId in assetGraph) {
128-
final importedAssetIds = <AssetId>{};
129-
130-
// Read library.
131-
final library = await buildStep.resolver.libraryFor(assetId);
132-
133-
for (final fragment in library.fragments) {
134-
for (final import in fragment.importedLibraries) {
135-
//final uri = Uri.parse(import.source.uri.toString());
136-
// TODO:Iterate over all fragments.
137-
final uri = import.uri;
138-
// Skip if uri scheme is not "package" or "asset".
139-
if (uri.scheme == 'package' ||
140-
uri.scheme == 'asset' ||
141-
uri.scheme == '') {
142-
// Normalise uri to handle relative and package import directives.
143-
final importedAssetId = AssetId.resolve(uri, from: assetId);
144-
// Add vertex matching import directive.
145-
if (assetMap[importedAssetId.uri] != null) {
146-
importedAssetIds.add(assetMap[importedAssetId.uri]!);
147-
}
181+
if (assetGraph.isAcyclic) {
182+
log.info('SyntheticBuilder: Assets sortable. ');
183+
// The graph is acyclic, that is the assetIds can be sorted in
184+
// topological order.
185+
final topologicalOrdering = assetGraph.sortedTopologicalOrdering;
186+
final result = <AssetId>{};
187+
log.fine(topologicalOrdering);
188+
for (final assetId in topologicalOrdering!) {
189+
if (assetIds.contains(assetId)) {
190+
result.add(assetId);
191+
}
192+
}
193+
return result;
194+
} else {
195+
// The graph is not acyclic but the relevant assetIds may still be
196+
// sorted in order of dependence if the graph cycle does not lead
197+
// to input file including each other.
198+
// Note: Input files include each other if their assetIds are located
199+
// in the same strongly connected component.
200+
final components = assetGraph.stronglyConnectedComponents;
201+
202+
final assetsInComponent = HashSet.of([]);
203+
bool isQuasiSortable = true;
204+
205+
componentLoop:
206+
for (final component in components) {
207+
// Start with an empty set when proceeding to the next component!
208+
assetsInComponent.clear();
209+
for (final assetId in assetIds) {
210+
if (component.contains(assetId)) {
211+
assetsInComponent.add(assetId);
212+
}
213+
if (assetsInComponent.length > 1) {
214+
// Two assets in the same component!
215+
// The files depend on each other.
216+
isQuasiSortable = false;
217+
break componentLoop;
148218
}
149219
}
150220
}
151-
assetGraph.addEdges(assetId, importedAssetIds);
152-
}
153-
154-
final topologicalOrdering = assetGraph.sortedTopologicalOrdering;
155-
156-
if (topologicalOrdering == null) {
157-
// Find the first cycle
158-
final cycle = assetGraph.cycle;
159221

160-
throw ErrorOf<SyntheticBuilder>(
161-
message: 'Circular dependency detected.',
162-
expectedState:
163-
'Input files must not include each other. '
164-
'Alternatively, set constructor parameter "sortAssets: false".',
165-
invalidState: 'File ${cycle.join(' imports ')}.',
166-
);
222+
if (isQuasiSortable) {
223+
log.info('SyntheticBuilder: Assets quasi-sortable.');
224+
final sortedAssets = components.fold(
225+
<AssetId>[],
226+
(flattendList, component) => flattendList
227+
..addAll(component.where((assetId) => assetIds.contains(assetId))),
228+
);
229+
return sortedAssets.toSet(); //
230+
} else {
231+
final message = assetGraph
232+
.path(assetsInComponent.first, assetsInComponent.first)
233+
.map((assetId) => assetId.path);
234+
final invalidState = message.join(' imports ');
235+
236+
throw ErrorOf<SyntheticBuilder>(
237+
message: 'Circular dependency detected.',
238+
expectedState:
239+
'Input files must not include each other. '
240+
'Alternatively, consider setting builder parameter '
241+
'<sortAssets: false>. See builder.yaml.',
242+
invalidState: invalidState,
243+
);
244+
}
167245
}
168-
169-
// Return reversed topological ordering of asset ids.
170-
return topologicalOrdering;
171246
}
172247
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: MergingBuilder reads several input files and writes merged output
44
to one file. StandaloneBuilder enables output to a custom folder with custom
55
output file name.
66

7-
version: 0.2.8
7+
version: 0.3.0
88

99
homepage: https://github.com/simphotonics/merging_builder
1010

0 commit comments

Comments
 (0)