Line data Source code
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 : import 'package:charcode/charcode.dart';
6 : import 'package:source_span/source_span.dart';
7 :
8 : import 'exception.dart';
9 : import 'utils.dart';
10 :
11 : /// When compiled to JS, forward slashes are always escaped in [RegExp.pattern].
12 : ///
13 : /// See issue 17998.
14 : final _slashAutoEscape = new RegExp("/").pattern == "\\/";
15 :
16 : /// A class that scans through a string using [Pattern]s.
17 : class StringScanner {
18 : /// The URL of the source of the string being scanned.
19 : ///
20 : /// This is used for error reporting. It may be `null`, indicating that the
21 : /// source URL is unknown or unavailable.
22 : final Uri sourceUrl;
23 :
24 : /// The string being scanned through.
25 : final String string;
26 :
27 : /// The current position of the scanner in the string, in characters.
28 0 : int get position => _position;
29 : set position(int position) {
30 0 : if (position < 0 || position > string.length) {
31 0 : throw new ArgumentError("Invalid position $position");
32 : }
33 :
34 0 : _position = position;
35 0 : _lastMatch = null;
36 : }
37 : int _position = 0;
38 :
39 : /// The data about the previous match made by the scanner.
40 : ///
41 : /// If the last match failed, this will be `null`.
42 : Match get lastMatch {
43 : // Lazily unset [_lastMatch] so that we avoid extra assignments in
44 : // character-by-character methods that are used in core loops.
45 0 : if (_position != _lastMatchPosition) _lastMatch = null;
46 0 : return _lastMatch;
47 : }
48 : Match _lastMatch;
49 : int _lastMatchPosition;
50 :
51 : /// The portion of the string that hasn't yet been scanned.
52 0 : String get rest => string.substring(position);
53 :
54 : /// Whether the scanner has completely consumed [string].
55 0 : bool get isDone => position == string.length;
56 :
57 : /// Creates a new [StringScanner] that starts scanning from [position].
58 : ///
59 : /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the
60 : /// URL of the source of the string being scanned, if available. It can be
61 : /// a [String], a [Uri], or `null`.
62 : StringScanner(this.string, {sourceUrl, int position})
63 0 : : sourceUrl = sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl {
64 0 : if (position != null) this.position = position;
65 : }
66 :
67 : /// Consumes a single character and returns its character code.
68 : ///
69 : /// This throws a [FormatException] if the string has been fully consumed. It
70 : /// doesn't affect [lastMatch].
71 : int readChar() {
72 0 : if (isDone) _fail("more input");
73 0 : return string.codeUnitAt(_position++);
74 : }
75 :
76 : /// Returns the character code of the character [offset] away from [position].
77 : ///
78 : /// [offset] defaults to zero, and may be negative to inspect already-consumed
79 : /// characters.
80 : ///
81 : /// This returns `null` if [offset] points outside the string. It doesn't
82 : /// affect [lastMatch].
83 : int peekChar([int offset]) {
84 : if (offset == null) offset = 0;
85 0 : var index = position + offset;
86 0 : if (index < 0 || index >= string.length) return null;
87 0 : return string.codeUnitAt(index);
88 : }
89 :
90 : /// If the next character in the string is [character], consumes it.
91 : ///
92 : /// Returns whether or not [character] was consumed.
93 : bool scanChar(int character) {
94 0 : if (isDone) return false;
95 0 : if (string.codeUnitAt(_position) != character) return false;
96 0 : _position++;
97 : return true;
98 : }
99 :
100 : /// If the next character in the string is [character], consumes it.
101 : ///
102 : /// If [character] could not be consumed, throws a [FormatException]
103 : /// describing the position of the failure. [name] is used in this error as
104 : /// the expected name of the character being matched; if it's `null`, the
105 : /// character itself is used instead.
106 : void expectChar(int character, {String name}) {
107 0 : if (scanChar(character)) return;
108 :
109 : if (name == null) {
110 0 : if (character == $backslash) {
111 : name = r'"\"';
112 0 : } else if (character == $double_quote) {
113 : name = r'"\""';
114 : } else {
115 0 : name = '"${new String.fromCharCode(character)}"';
116 : }
117 : }
118 :
119 0 : _fail(name);
120 : }
121 :
122 : /// If [pattern] matches at the current position of the string, scans forward
123 : /// until the end of the match.
124 : ///
125 : /// Returns whether or not [pattern] matched.
126 : bool scan(Pattern pattern) {
127 0 : var success = matches(pattern);
128 : if (success) {
129 0 : _position = _lastMatch.end;
130 0 : _lastMatchPosition = _position;
131 : }
132 : return success;
133 : }
134 :
135 : /// If [pattern] matches at the current position of the string, scans forward
136 : /// until the end of the match.
137 : ///
138 : /// If [pattern] did not match, throws a [FormatException] describing the
139 : /// position of the failure. [name] is used in this error as the expected name
140 : /// of the pattern being matched; if it's `null`, the pattern itself is used
141 : /// instead.
142 : void expect(Pattern pattern, {String name}) {
143 0 : if (scan(pattern)) return;
144 :
145 : if (name == null) {
146 0 : if (pattern is RegExp) {
147 0 : var source = pattern.pattern;
148 0 : if (!_slashAutoEscape) source = source.replaceAll("/", "\\/");
149 0 : name = "/$source/";
150 : } else {
151 : name =
152 0 : pattern.toString().replaceAll("\\", "\\\\").replaceAll('"', '\\"');
153 0 : name = '"$name"';
154 : }
155 : }
156 0 : _fail(name);
157 : }
158 :
159 : /// If the string has not been fully consumed, this throws a
160 : /// [FormatException].
161 : void expectDone() {
162 0 : if (isDone) return;
163 0 : _fail("no more input");
164 : }
165 :
166 : /// Returns whether or not [pattern] matches at the current position of the
167 : /// string.
168 : ///
169 : /// This doesn't move the scan pointer forward.
170 : bool matches(Pattern pattern) {
171 0 : _lastMatch = pattern.matchAsPrefix(string, position);
172 0 : _lastMatchPosition = _position;
173 0 : return _lastMatch != null;
174 : }
175 :
176 : /// Returns the substring of [string] between [start] and [end].
177 : ///
178 : /// Unlike [String.substring], [end] defaults to [position] rather than the
179 : /// end of the string.
180 : String substring(int start, [int end]) {
181 0 : if (end == null) end = position;
182 0 : return string.substring(start, end);
183 : }
184 :
185 : /// Throws a [FormatException] with [message] as well as a detailed
186 : /// description of the location of the error in the string.
187 : ///
188 : /// [match] is the match information for the span of the string with which the
189 : /// error is associated. This should be a match returned by this scanner's
190 : /// [lastMatch] property. By default, the error is associated with the last
191 : /// match.
192 : ///
193 : /// If [position] and/or [length] are passed, they are used as the error span
194 : /// instead. If only [length] is passed, [position] defaults to the current
195 : /// position; if only [position] is passed, [length] defaults to 0.
196 : ///
197 : /// It's an error to pass [match] at the same time as [position] or [length].
198 : void error(String message, {Match match, int position, int length}) {
199 0 : validateErrorArgs(string, match, position, length);
200 :
201 0 : if (match == null && position == null && length == null) match = lastMatch;
202 : if (position == null) {
203 0 : position = match == null ? this.position : match.start;
204 : }
205 0 : if (length == null) length = match == null ? 0 : match.end - match.start;
206 :
207 0 : var sourceFile = new SourceFile.fromString(string, url: sourceUrl);
208 0 : var span = sourceFile.span(position, position + length);
209 0 : throw new StringScannerException(message, span, string);
210 : }
211 :
212 : // TODO(nweiz): Make this handle long lines more gracefully.
213 : /// Throws a [FormatException] describing that [name] is expected at the
214 : /// current position in the string.
215 : void _fail(String name) {
216 0 : error("expected $name.", position: this.position, length: 0);
217 : }
218 : }
|