OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, 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 library source_span.file; | |
6 | |
7 import 'dart:math' as math; | |
8 import 'dart:typed_data'; | |
9 | |
10 import 'location.dart'; | |
11 import 'location_mixin.dart'; | |
12 import 'span.dart'; | |
13 import 'span_mixin.dart'; | |
14 import 'span_with_context.dart'; | |
15 | |
16 // Constants to determine end-of-lines. | |
17 const int _LF = 10; | |
18 const int _CR = 13; | |
19 | |
20 /// A class representing a source file. | |
21 /// | |
22 /// This doesn't necessarily have to correspond to a file on disk, just a chunk | |
23 /// of text usually with a URL associated with it. | |
24 class SourceFile { | |
25 /// The URL where the source file is located. | |
26 /// | |
27 /// This may be null, indicating that the URL is unknown or unavailable. | |
28 final Uri url; | |
29 | |
30 /// An array of offsets for each line beginning in the file. | |
31 /// | |
32 /// Each offset refers to the first character *after* the newline. If the | |
33 /// source file has a trailing newline, the final offset won't actually be in | |
34 /// the file. | |
35 final _lineStarts = <int>[0]; | |
36 | |
37 /// The code points of the characters in the file. | |
38 final Uint32List _decodedChars; | |
39 | |
40 /// The length of the file in characters. | |
41 int get length => _decodedChars.length; | |
42 | |
43 /// The number of lines in the file. | |
44 int get lines => _lineStarts.length; | |
45 | |
46 /// The line that the offset fell on the last time [getLine] was called. | |
47 /// | |
48 /// In many cases, sequential calls to getLine() are for nearby, usually | |
49 /// increasing offsets. In that case, we can find the line for an offset | |
50 /// quickly by first checking to see if the offset is on the same line as the | |
51 /// previous result. | |
52 int _cachedLine; | |
53 | |
54 /// Creates a new source file from [text]. | |
55 /// | |
56 /// [url] may be either a [String], a [Uri], or `null`. | |
57 SourceFile(String text, {url}) | |
58 : this.decoded(text.runes, url: url); | |
59 | |
60 /// Creates a new source file from a list of decoded characters. | |
61 /// | |
62 /// [url] may be either a [String], a [Uri], or `null`. | |
63 SourceFile.decoded(Iterable<int> decodedChars, {url}) | |
64 : url = url is String ? Uri.parse(url) : url, | |
65 _decodedChars = new Uint32List.fromList(decodedChars.toList()) { | |
66 for (var i = 0; i < _decodedChars.length; i++) { | |
67 var c = _decodedChars[i]; | |
68 if (c == _CR) { | |
69 // Return not followed by newline is treated as a newline | |
70 var j = i + 1; | |
71 if (j >= _decodedChars.length || _decodedChars[j] != _LF) c = _LF; | |
72 } | |
73 if (c == _LF) _lineStarts.add(i + 1); | |
74 } | |
75 } | |
76 | |
77 /// Returns a span in [this] from [start] to [end] (exclusive). | |
78 /// | |
79 /// If [end] isn't passed, it defaults to the end of the file. | |
80 FileSpan span(int start, [int end]) { | |
81 if (end == null) end = length - 1; | |
82 return new _FileSpan(this, start, end); | |
83 } | |
84 | |
85 /// Returns a location in [this] at [offset]. | |
86 FileLocation location(int offset) => new FileLocation._(this, offset); | |
87 | |
88 /// Gets the 0-based line corresponding to [offset]. | |
89 int getLine(int offset) { | |
90 if (offset < 0) { | |
91 throw new RangeError("Offset may not be negative, was $offset."); | |
92 } else if (offset > length) { | |
93 throw new RangeError("Offset $offset must not be greater than the number " | |
94 "of characters in the file, $length."); | |
95 } | |
96 | |
97 if (offset < _lineStarts.first) return -1; | |
98 if (offset >= _lineStarts.last) return _lineStarts.length - 1; | |
99 | |
100 if (_isNearCachedLine(offset)) return _cachedLine; | |
101 | |
102 _cachedLine = _binarySearch(offset) - 1; | |
103 return _cachedLine; | |
104 } | |
105 | |
106 /// Returns `true` if [offset] is near [_cachedLine]. | |
107 /// | |
108 /// Checks on [_cachedLine] and the next line. If it's on the next line, it | |
109 /// updates [_cachedLine] to point to that. | |
110 bool _isNearCachedLine(int offset) { | |
111 if (_cachedLine == null) return false; | |
112 | |
113 // See if it's before the cached line. | |
114 if (offset < _lineStarts[_cachedLine]) return false; | |
115 | |
116 // See if it's on the cached line. | |
117 if (_cachedLine >= _lineStarts.length - 1 || | |
118 offset < _lineStarts[_cachedLine + 1]) { | |
119 return true; | |
120 } | |
121 | |
122 // See if it's on the next line. | |
123 if (_cachedLine >= _lineStarts.length - 2 || | |
124 offset < _lineStarts[_cachedLine + 2]) { | |
125 _cachedLine++; | |
126 return true; | |
127 } | |
128 | |
129 return false; | |
130 } | |
131 | |
132 /// Binary search through [_lineStarts] to find the line containing [offset]. | |
133 /// | |
134 /// Returns the index of the line in [_lineStarts]. | |
135 int _binarySearch(int offset) { | |
136 int min = 0; | |
137 int max = _lineStarts.length - 1; | |
138 while (min < max) { | |
139 var half = min + ((max - min) ~/ 2); | |
140 if (_lineStarts[half] > offset) { | |
141 max = half; | |
142 } else { | |
143 min = half + 1; | |
144 } | |
145 } | |
146 | |
147 return max; | |
148 } | |
149 | |
150 /// Gets the 0-based column corresponding to [offset]. | |
151 /// | |
152 /// If [line] is passed, it's assumed to be the line containing [offset] and | |
153 /// is used to more efficiently compute the column. | |
154 int getColumn(int offset, {int line}) { | |
155 if (offset < 0) { | |
156 throw new RangeError("Offset may not be negative, was $offset."); | |
157 } else if (offset > length) { | |
158 throw new RangeError("Offset $offset must be not be greater than the " | |
159 "number of characters in the file, $length."); | |
160 } | |
161 | |
162 if (line == null) { | |
163 line = getLine(offset); | |
164 } else if (line < 0) { | |
165 throw new RangeError("Line may not be negative, was $line."); | |
166 } else if (line >= lines) { | |
167 throw new RangeError("Line $line must be less than the number of " | |
168 "lines in the file, $lines."); | |
169 } | |
170 | |
171 var lineStart = _lineStarts[line]; | |
172 if (lineStart > offset) { | |
173 throw new RangeError("Line $line comes after offset $offset."); | |
174 } | |
175 | |
176 return offset - lineStart; | |
177 } | |
178 | |
179 /// Gets the offset for a [line] and [column]. | |
180 /// | |
181 /// [column] defaults to 0. | |
182 int getOffset(int line, [int column]) { | |
183 if (column == null) column = 0; | |
184 | |
185 if (line < 0) { | |
186 throw new RangeError("Line may not be negative, was $line."); | |
187 } else if (line >= lines) { | |
188 throw new RangeError("Line $line must be less than the number of " | |
189 "lines in the file, $lines."); | |
190 } else if (column < 0) { | |
191 throw new RangeError("Column may not be negative, was $column."); | |
192 } | |
193 | |
194 var result = _lineStarts[line] + column; | |
195 if (result > length || | |
196 (line + 1 < lines && result >= _lineStarts[line + 1])) { | |
197 throw new RangeError("Line $line doesn't have $column columns."); | |
198 } | |
199 | |
200 return result; | |
201 } | |
202 | |
203 /// Returns the text of the file from [start] to [end] (exclusive). | |
204 /// | |
205 /// If [end] isn't passed, it defaults to the end of the file. | |
206 String getText(int start, [int end]) => | |
207 new String.fromCharCodes(_decodedChars.sublist(start, end)); | |
208 } | |
209 | |
210 /// A [SourceLocation] within a [SourceFile]. | |
211 /// | |
212 /// Unlike the base [SourceLocation], [FileLocation] lazily computes its line | |
213 /// and column values based on its offset and the contents of [file]. | |
214 /// | |
215 /// A [FileLocation] can be created using [SourceFile.location]. | |
216 class FileLocation extends SourceLocationMixin implements SourceLocation { | |
217 /// The [file] that [this] belongs to. | |
218 final SourceFile file; | |
219 | |
220 final int offset; | |
221 Uri get sourceUrl => file.url; | |
222 int get line => file.getLine(offset); | |
223 int get column => file.getColumn(offset); | |
224 | |
225 FileLocation._(this.file, this.offset) { | |
226 if (offset < 0) { | |
227 throw new RangeError("Offset may not be negative, was $offset."); | |
228 } else if (offset > file.length) { | |
229 throw new RangeError("Offset $offset must not be greater than the number " | |
230 "of characters in the file, ${file.length}."); | |
231 } | |
232 } | |
233 | |
234 FileSpan pointSpan() => new _FileSpan(file, offset, offset); | |
235 } | |
236 | |
237 /// A [SourceSpan] within a [SourceFile]. | |
238 /// | |
239 /// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column | |
240 /// values based on its offset and the contents of [file]. [FileSpan.message] is | |
241 /// also able to provide more context then [SourceSpan.message], and | |
242 /// [FileSpan.union] will return a [FileSpan] if possible. | |
243 /// | |
244 /// A [FileSpan] can be created using [SourceFile.span]. | |
245 abstract class FileSpan implements SourceSpanWithContext { | |
246 /// The [file] that [this] belongs to. | |
247 SourceFile get file; | |
248 | |
249 /// Returns a new span that covers both [this] and [other]. | |
250 /// | |
251 /// Unlike [union], [other] may be disjoint from [this]. If it is, the text | |
252 /// between the two will be covered by the returned span. | |
253 FileSpan expand(FileSpan other); | |
254 } | |
255 | |
256 /// The implementation of [FileSpan]. | |
257 /// | |
258 /// This is split into a separate class so that `is _FileSpan` checks can be run | |
259 /// to make certain operations more efficient. If we used `is FileSpan`, that | |
260 /// would break if external classes implemented the interface. | |
261 class _FileSpan extends SourceSpanMixin implements FileSpan { | |
262 final SourceFile file; | |
263 | |
264 /// The offset of the beginning of the span. | |
265 /// | |
266 /// [start] is lazily generated from this to avoid allocating unnecessary | |
267 /// objects. | |
268 final int _start; | |
269 | |
270 /// The offset of the end of the span. | |
271 /// | |
272 /// [end] is lazily generated from this to avoid allocating unnecessary | |
273 /// objects. | |
274 final int _end; | |
275 | |
276 Uri get sourceUrl => file.url; | |
277 int get length => _end - _start; | |
278 FileLocation get start => new FileLocation._(file, _start); | |
279 FileLocation get end => new FileLocation._(file, _end); | |
280 String get text => file.getText(_start, _end); | |
281 String get context => file.getText(file.getOffset(start.line), | |
282 end.line == file.lines - 1 ? null : file.getOffset(end.line + 1)); | |
283 | |
284 _FileSpan(this.file, this._start, this._end) { | |
285 if (_end < _start) { | |
286 throw new ArgumentError('End $_end must come after start $_start.'); | |
287 } else if (_end > file.length) { | |
288 throw new RangeError("End $_end must not be greater than the number " | |
289 "of characters in the file, ${file.length}."); | |
290 } else if (_start < 0) { | |
291 throw new RangeError("Start may not be negative, was $_start."); | |
292 } | |
293 } | |
294 | |
295 int compareTo(SourceSpan other) { | |
296 if (other is! _FileSpan) return super.compareTo(other); | |
297 | |
298 _FileSpan otherFile = other; | |
299 var result = _start.compareTo(otherFile._start); | |
300 return result == 0 ? _end.compareTo(otherFile._end) : result; | |
301 } | |
302 | |
303 SourceSpan union(SourceSpan other) { | |
304 if (other is! FileSpan) return super.union(other); | |
305 | |
306 | |
307 _FileSpan span = expand(other); | |
308 | |
309 if (other is _FileSpan) { | |
310 if (this._start > other._end || other._start > this._end) { | |
311 throw new ArgumentError("Spans $this and $other are disjoint."); | |
312 } | |
313 } else { | |
314 if (this._start > other.end.offset || other.start.offset > this._end) { | |
315 throw new ArgumentError("Spans $this and $other are disjoint."); | |
316 } | |
317 } | |
318 | |
319 return span; | |
320 } | |
321 | |
322 bool operator ==(other) { | |
323 if (other is! FileSpan) return super == other; | |
324 if (other is! _FileSpan) { | |
325 return super == other && sourceUrl == other.sourceUrl; | |
326 } | |
327 | |
328 return _start == other._start && _end == other._end && | |
329 sourceUrl == other.sourceUrl; | |
330 } | |
331 | |
332 // Eliminates dart2js warning about overriding `==`, but not `hashCode` | |
333 int get hashCode => super.hashCode; | |
334 | |
335 /// Returns a new span that covers both [this] and [other]. | |
336 /// | |
337 /// Unlike [union], [other] may be disjoint from [this]. If it is, the text | |
338 /// between the two will be covered by the returned span. | |
339 FileSpan expand(FileSpan other) { | |
340 if (sourceUrl != other.sourceUrl) { | |
341 throw new ArgumentError("Source URLs \"${sourceUrl}\" and " | |
342 " \"${other.sourceUrl}\" don't match."); | |
343 } | |
344 | |
345 if (other is _FileSpan) { | |
346 var start = math.min(this._start, other._start); | |
347 var end = math.max(this._end, other._end); | |
348 return new _FileSpan(file, start, end); | |
349 } else { | |
350 var start = math.min(this._start, other.start.offset); | |
351 var end = math.max(this._end, other.end.offset); | |
352 return new _FileSpan(file, start, end); | |
353 } | |
354 } | |
355 } | |
OLD | NEW |