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 : /// A class that scans through a string using [Pattern]s. 12 : class StringScanner { 13 : /// The URL of the source of the string being scanned. 14 : /// 15 : /// This is used for error reporting. It may be `null`, indicating that the 16 : /// source URL is unknown or unavailable. 17 : final Uri? sourceUrl; 18 : 19 : /// The string being scanned through. 20 : final String string; 21 : 22 : /// The current position of the scanner in the string, in characters. 23 16 : int get position => _position; 24 0 : set position(int position) { 25 0 : if (position < 0 || position > string.length) { 26 0 : throw ArgumentError('Invalid position $position'); 27 : } 28 : 29 0 : _position = position; 30 0 : _lastMatch = null; 31 : } 32 : 33 : int _position = 0; 34 : 35 : /// The data about the previous match made by the scanner. 36 : /// 37 : /// If the last match failed, this will be `null`. 38 8 : Match? get lastMatch { 39 : // Lazily unset [_lastMatch] so that we avoid extra assignments in 40 : // character-by-character methods that are used in core loops. 41 24 : if (_position != _lastMatchPosition) _lastMatch = null; 42 8 : return _lastMatch; 43 : } 44 : 45 : Match? _lastMatch; 46 : int? _lastMatchPosition; 47 : 48 : /// The portion of the string that hasn't yet been scanned. 49 0 : String get rest => string.substring(position); 50 : 51 : /// Whether the scanner has completely consumed [string]. 52 40 : bool get isDone => position == string.length; 53 : 54 : /// Creates a new [StringScanner] that starts scanning from [position]. 55 : /// 56 : /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the 57 : /// URL of the source of the string being scanned, if available. It can be 58 : /// a [String], a [Uri], or `null`. 59 8 : StringScanner(this.string, {sourceUrl, int? position}) 60 : : sourceUrl = sourceUrl == null 61 : ? null 62 0 : : sourceUrl is String 63 0 : ? Uri.parse(sourceUrl) 64 : : sourceUrl as Uri { 65 0 : if (position != null) this.position = position; 66 : } 67 : 68 : /// Consumes a single character and returns its character code. 69 : /// 70 : /// This throws a [FormatException] if the string has been fully consumed. It 71 : /// doesn't affect [lastMatch]. 72 0 : int readChar() { 73 0 : if (isDone) _fail('more input'); 74 0 : return string.codeUnitAt(_position++); 75 : } 76 : 77 : /// Returns the character code of the character [offset] away from [position]. 78 : /// 79 : /// [offset] defaults to zero, and may be negative to inspect already-consumed 80 : /// characters. 81 : /// 82 : /// This returns `null` if [offset] points outside the string. It doesn't 83 : /// affect [lastMatch]. 84 5 : int? peekChar([int? offset]) { 85 : offset ??= 0; 86 10 : final index = position + offset; 87 20 : if (index < 0 || index >= string.length) return null; 88 10 : return string.codeUnitAt(index); 89 : } 90 : 91 : /// If the next character in the string is [character], consumes it. 92 : /// 93 : /// Returns whether or not [character] was consumed. 94 0 : bool scanChar(int character) { 95 0 : if (isDone) return false; 96 0 : if (string.codeUnitAt(_position) != character) return false; 97 0 : _position++; 98 : return true; 99 : } 100 : 101 : /// If the next character in the string is [character], consumes it. 102 : /// 103 : /// If [character] could not be consumed, throws a [FormatException] 104 : /// describing the position of the failure. [name] is used in this error as 105 : /// the expected name of the character being matched; if it's `null`, the 106 : /// character itself is used instead. 107 0 : void expectChar(int character, {String? name}) { 108 0 : if (scanChar(character)) return; 109 : 110 : if (name == null) { 111 0 : if (character == $backslash) { 112 : name = r'"\"'; 113 0 : } else if (character == $double_quote) { 114 : name = r'"\""'; 115 : } else { 116 0 : name = '"${String.fromCharCode(character)}"'; 117 : } 118 : } 119 : 120 0 : _fail(name); 121 : } 122 : 123 : /// If [pattern] matches at the current position of the string, scans forward 124 : /// until the end of the match. 125 : /// 126 : /// Returns whether or not [pattern] matched. 127 8 : bool scan(Pattern pattern) { 128 8 : final success = matches(pattern); 129 : if (success) { 130 24 : _position = _lastMatch!.end; 131 16 : _lastMatchPosition = _position; 132 : } 133 : return success; 134 : } 135 : 136 : /// If [pattern] matches at the current position of the string, scans forward 137 : /// until the end of the match. 138 : /// 139 : /// If [pattern] did not match, throws a [FormatException] describing the 140 : /// position of the failure. [name] is used in this error as the expected name 141 : /// of the pattern being matched; if it's `null`, the pattern itself is used 142 : /// instead. 143 8 : void expect(Pattern pattern, {String? name}) { 144 8 : if (scan(pattern)) return; 145 : 146 : if (name == null) { 147 0 : if (pattern is RegExp) { 148 0 : final source = pattern.pattern; 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 5 : void expectDone() { 162 5 : 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 8 : bool matches(Pattern pattern) { 171 32 : _lastMatch = pattern.matchAsPrefix(string, position); 172 16 : _lastMatchPosition = _position; 173 8 : 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 0 : String substring(int start, [int? end]) { 181 0 : 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 0 : Never 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 0 : position ??= match == null ? this.position : match.start; 203 0 : length ??= match == null ? 0 : match.end - match.start; 204 : 205 0 : final sourceFile = SourceFile.fromString(string, url: sourceUrl); 206 0 : final span = sourceFile.span(position, position + length); 207 0 : throw StringScannerException(message, span, string); 208 : } 209 : 210 : // TODO(nweiz): Make this handle long lines more gracefully. 211 : /// Throws a [FormatException] describing that [name] is expected at the 212 : /// current position in the string. 213 0 : Never _fail(String name) { 214 0 : error('expected $name.', position: position, length: 0); 215 : } 216 : }