Skip to content

Commit dd7b3a3

Browse files
authored
[rfw] Add support for tracking source locations of BlobNodes. (#5876)
Fixes flutter/flutter#141070.
1 parent d21f3b8 commit dd7b3a3

File tree

8 files changed

+598
-119
lines changed

8 files changed

+598
-119
lines changed

packages/rfw/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.0.17
2+
3+
* Adds support for tracking source locations of `BlobNode`s and
4+
finding `BlobNode`s from the widget tree (`BlobNode.source` and
5+
`Runtime.blobNodeFor` respectively).
6+
17
## 1.0.16
28

39
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.

packages/rfw/lib/src/dart/model.dart

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,157 @@ typedef DynamicMap = Map<String, Object?>;
2020
/// Part of the data type for [DynamicContent] objects.
2121
typedef DynamicList = List<Object?>;
2222

23+
/// Reference to a location in a source file.
24+
///
25+
/// This is used in a [SourceRange] object to indicate the location of a
26+
/// [BlobNode] in the original source text.
27+
///
28+
/// Locations are given as offsets (in UTF-16 code units) into the decoded
29+
/// string.
30+
///
31+
/// See also:
32+
///
33+
/// * [BlobNode.source], which exposes the source location of a [BlobNode].
34+
@immutable
35+
class SourceLocation implements Comparable<SourceLocation> {
36+
/// Create a [SourceLocation] object.
37+
///
38+
/// The [source] and [offset] properties are initialized from the
39+
/// given arguments.
40+
const SourceLocation(this.source, this.offset);
41+
42+
/// An object that identifies the file or other origin of the source.
43+
///
44+
/// For files parsed using [parseLibraryFile], this is the value that
45+
/// is given as the `sourceIdentifier` argument.
46+
final Object source;
47+
48+
/// The offset of the given source location, in UTF-16 code units.
49+
final int offset;
50+
51+
@override
52+
int compareTo(SourceLocation other) {
53+
if (source != other.source) {
54+
throw StateError('Cannot compare locations from different sources.');
55+
}
56+
return offset - other.offset;
57+
}
58+
59+
@override
60+
bool operator ==(Object other) {
61+
if (other.runtimeType != SourceLocation) {
62+
return false;
63+
}
64+
return other is SourceLocation
65+
&& source == other.source
66+
&& offset == other.offset;
67+
}
68+
69+
@override
70+
int get hashCode => Object.hash(source, offset);
71+
72+
/// Whether this location is earlier in the file than `other`.
73+
///
74+
/// Can only be used to compare locations in the same [source].
75+
bool operator <(SourceLocation other) {
76+
if (source != other.source) {
77+
throw StateError('Cannot compare locations from different sources.');
78+
}
79+
return offset < other.offset;
80+
}
81+
82+
/// Whether this location is later in the file than `other`.
83+
///
84+
/// Can only be used to compare locations in the same [source].
85+
bool operator >(SourceLocation other) {
86+
if (source != other.source) {
87+
throw StateError('Cannot compare locations from different sources.');
88+
}
89+
return offset > other.offset;
90+
}
91+
92+
/// Whether this location is earlier in the file than `other`, or equal to
93+
/// `other`.
94+
///
95+
/// Can only be used to compare locations in the same [source].
96+
bool operator <=(SourceLocation other) {
97+
if (source != other.source) {
98+
throw StateError('Cannot compare locations from different sources.');
99+
}
100+
return offset <= other.offset;
101+
}
102+
103+
/// Whether this location is later in the file than `other`, or equal to
104+
/// `other`.
105+
///
106+
/// Can only be used to compare locations in the same [source].
107+
bool operator >=(SourceLocation other) {
108+
if (source != other.source) {
109+
throw StateError('Cannot compare locations from different sources.');
110+
}
111+
return offset >= other.offset;
112+
}
113+
114+
@override
115+
String toString() {
116+
return '$source@$offset';
117+
}
118+
}
119+
120+
/// Reference to a range of a source file.
121+
///
122+
/// This is used to indicate the region of a source file that corresponds to a
123+
/// particular [BlobNode].
124+
///
125+
/// By default, [BlobNode]s are not associated with [SourceRange]s. Source
126+
/// location information can be enabled for the [parseLibraryFile] parser by
127+
/// providing the `sourceIdentifier` argument.
128+
///
129+
/// See also:
130+
///
131+
/// * [BlobNode.source], which exposes the source location of a [BlobNode].
132+
@immutable
133+
class SourceRange {
134+
/// Create a [SourceRange] object.
135+
///
136+
/// The [start] and [end] locations are initialized from the given arguments.
137+
///
138+
/// They must have identical [SourceLocation.source] objects.
139+
SourceRange(this.start, this.end)
140+
: assert(start.source == end.source, 'The start and end locations have inconsistent source information.'),
141+
assert(start < end, 'The start location must be before the end location.');
142+
143+
/// The start of a contiguous region of a source file that corresponds to a
144+
/// particular [BlobNode].
145+
///
146+
/// The range contains the start.
147+
final SourceLocation start;
148+
149+
/// The end of a contiguous region of a source file that corresponds to a
150+
/// particular [BlobNode].
151+
///
152+
/// The range does not contain the end.
153+
final SourceLocation end;
154+
155+
@override
156+
bool operator ==(Object other) {
157+
if (other.runtimeType != SourceRange) {
158+
return false;
159+
}
160+
return other is SourceRange
161+
&& start == other.start
162+
&& end == other.end;
163+
}
164+
165+
@override
166+
int get hashCode => Object.hash(start, end);
167+
168+
@override
169+
String toString() {
170+
return '${start.source}@${start.offset}..${end.offset}';
171+
}
172+
}
173+
23174
/// Base class of nodes that appear in the output of [decodeDataBlob] and
24175
/// [decodeLibraryBlob].
25176
///
@@ -35,6 +186,59 @@ abstract class BlobNode {
35186
/// Abstract const constructor. This constructor enables subclasses to provide
36187
/// const constructors so that they can be used in const expressions.
37188
const BlobNode();
189+
190+
// We use an [Expando] so that there is no (or minimal) overhead in production
191+
// environments that don't need to track source locations. It would be cleaner
192+
// to store the information directly on the [BlobNode], as then we could enforce
193+
// that that information is always propagated, instead of relying on remembering
194+
// to do so. However, that would require growing the size of every [BlobNode]
195+
// object, and would require additional logic even in the binary parser (which
196+
// does not track source locations currently).
197+
static final Expando<SourceRange> _sources = Expando<SourceRange>('BlobNode._sources');
198+
199+
/// The source location that corresponds to this [BlobNode], if known.
200+
///
201+
/// In normal use, this returns null. However, if source location tracking is
202+
/// enabled (e.g. by specifying the `sourceIdentifier` argument to
203+
/// [parseLibraryFile]), then this will return the range of the source file
204+
/// that corresponds to this [BlobNode].
205+
///
206+
/// A [BlobNode] can also be manually associated with a given [SourceRange]
207+
/// using [associateSource] or [propagateSource].
208+
SourceRange? get source {
209+
return _sources[this];
210+
}
211+
212+
/// Assign a [SourceRange] to this [BlobNode]'s [source] property.
213+
///
214+
/// Typically, this is used exclusively by the parser (notably,
215+
/// [parseLibraryFile]).
216+
///
217+
/// Tracking source location information introduces a memory overhead and
218+
/// should therefore only be used when necessary (e.g. for creating IDEs).
219+
///
220+
/// Calling this method replaces any existing association.
221+
void associateSource(SourceRange source) {
222+
_sources[this] = source;
223+
}
224+
225+
/// Assign another [BlobNode]'s [SourceRange] to this [BlobNode]'s [source]
226+
/// property.
227+
///
228+
/// Typically, this is used exclusively by the [Runtime].
229+
///
230+
/// If the `original` [BlobNode] is null or has no [source], then this has no
231+
/// effect. Otherwise, the [source] for this [BlobNode] is set to match that
232+
/// of the given `original` [BlobNode], replacing any existing association.
233+
void propagateSource(BlobNode? original) {
234+
if (original == null) {
235+
return;
236+
}
237+
final SourceRange? source = _sources[original];
238+
if (source != null) {
239+
_sources[this] = source;
240+
}
241+
}
38242
}
39243

40244
bool _listEquals<T>(List<T>? a, List<T>? b) {
@@ -192,7 +396,9 @@ class Switch extends BlobNode {
192396
/// [Switch] is an error.
193397
const Switch(this.input, this.outputs);
194398

195-
/// The value to switch on (after resolution).
399+
/// The value to switch on. This is typically a reference, e.g. an
400+
/// [ArgsReference], which must be resolved by the runtime to determine the
401+
/// actual value on which to switch.
196402
final Object input;
197403

198404
/// The cases for this switch. Keys correspond to values to compare with

0 commit comments

Comments
 (0)