Line data Source code
1 : // Copyright (c) 2016, 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:string_scanner/string_scanner.dart'; 6 : 7 : import 'token.dart'; 8 : 9 : /// A regular expression matching both whitespace and single-line comments. 10 : /// 11 : /// This will only match if consumes at least one character. 12 15 : final _whitespaceAndSingleLineComments = RegExp(r'([ \t\n]+|//[^\n]*(\n|$))+'); 13 : 14 : /// A regular expression matching the body of a multi-line comment, after `/*` 15 : /// but before `*/` or a nested `/*`. 16 : /// 17 : /// This will only match if it consumes at least one character. 18 0 : final _multiLineCommentBody = RegExp(r'([^/*]|/[^*]|\*[^/])+'); 19 : 20 : /// A regular expression matching a hyphenated identifier. 21 : /// 22 : /// This is like a standard Dart identifier, except that it can also contain 23 : /// hyphens. 24 15 : final _hyphenatedIdentifier = RegExp(r'[a-zA-Z_-][a-zA-Z0-9_-]*'); 25 : 26 : /// A scanner that converts a boolean selector string into a stream of tokens. 27 : class Scanner { 28 : /// The underlying string scanner. 29 : final SpanScanner _scanner; 30 : 31 : /// The next token to emit. 32 : Token? _next; 33 : 34 : /// Whether the scanner has emitted a [TokenType.endOfFile] token. 35 : bool _endOfFileEmitted = false; 36 : 37 10 : Scanner(String selector) : _scanner = SpanScanner(selector); 38 : 39 : /// Returns the next token that will be returned by [next]. 40 : /// 41 : /// Throws a [StateError] if a [TokenType.endOfFile] token has already been 42 : /// consumed. 43 15 : Token peek() => _next ??= _readNext(); 44 : 45 : /// Consumes and returns the next token in the stream. 46 : /// 47 : /// Throws a [StateError] if a [TokenType.endOfFile] token has already been 48 : /// consumed. 49 5 : Token next() { 50 10 : var token = _next ?? _readNext(); 51 15 : _endOfFileEmitted = token.type == TokenType.endOfFile; 52 5 : _next = null; 53 : return token; 54 : } 55 : 56 : /// If the next token matches [type], consumes it and returns `true`; 57 : /// otherwise, returns `false`. 58 : /// 59 : /// Throws a [StateError] if a [TokenType.endOfFile] token has already been 60 : /// consumed. 61 5 : bool scan(TokenType type) { 62 15 : if (peek().type != type) return false; 63 0 : next(); 64 : return true; 65 : } 66 : 67 : /// Scan and return the next token in the stream. 68 5 : Token _readNext() { 69 5 : if (_endOfFileEmitted) throw StateError('No more tokens.'); 70 : 71 5 : _consumeWhitespace(); 72 10 : if (_scanner.isDone) { 73 25 : return Token(TokenType.endOfFile, _scanner.spanFrom(_scanner.state)); 74 : } 75 : 76 10 : switch (_scanner.peekChar()) { 77 5 : case 0x28 /* ( */ : 78 0 : return _scanOperator(TokenType.leftParen); 79 5 : case 0x29 /* ) */ : 80 0 : return _scanOperator(TokenType.rightParen); 81 5 : case 0x3F /* ? */ : 82 0 : return _scanOperator(TokenType.questionMark); 83 5 : case 0x3A /* : */ : 84 0 : return _scanOperator(TokenType.colon); 85 5 : case 0x21 /* ! */ : 86 0 : return _scanOperator(TokenType.not); 87 5 : case 0x7C /* | */ : 88 0 : return _scanOr(); 89 5 : case 0x26 /* & */ : 90 0 : return _scanAnd(); 91 : default: 92 5 : return _scanIdentifier(); 93 : } 94 : } 95 : 96 : /// Scans a single-character operator and returns a token of type [type]. 97 : /// 98 : /// This assumes that the caller has already verified that the next character 99 : /// is correct for the given operator. 100 0 : Token _scanOperator(TokenType type) { 101 0 : var start = _scanner.state; 102 0 : _scanner.readChar(); 103 0 : return Token(type, _scanner.spanFrom(start)); 104 : } 105 : 106 : /// Scans a `||` operator and returns the appropriate token. 107 : /// 108 : /// This validates that the next two characters are `||`. 109 0 : Token _scanOr() { 110 0 : var start = _scanner.state; 111 0 : _scanner.expect('||'); 112 0 : return Token(TokenType.or, _scanner.spanFrom(start)); 113 : } 114 : 115 : /// Scans a `&&` operator and returns the appropriate token. 116 : /// 117 : /// This validates that the next two characters are `&&`. 118 0 : Token _scanAnd() { 119 0 : var start = _scanner.state; 120 0 : _scanner.expect('&&'); 121 0 : return Token(TokenType.and, _scanner.spanFrom(start)); 122 : } 123 : 124 : /// Scans and returns an identifier token. 125 5 : Token _scanIdentifier() { 126 15 : _scanner.expect(_hyphenatedIdentifier, name: 'expression'); 127 30 : return IdentifierToken(_scanner.lastMatch![0]!, _scanner.lastSpan!); 128 : } 129 : 130 : /// Consumes all whitespace and comments immediately following the cursor's 131 : /// current position. 132 5 : void _consumeWhitespace() { 133 15 : while (_scanner.scan(_whitespaceAndSingleLineComments) || 134 5 : _multiLineComment()) { 135 : // Do nothing. 136 : } 137 : } 138 : 139 : /// Consumes a single multi-line comment. 140 : /// 141 : /// Returns whether or not a comment was consumed. 142 5 : bool _multiLineComment() { 143 10 : if (!_scanner.scan('/*')) return false; 144 : 145 0 : while (_scanner.scan(_multiLineCommentBody) || _multiLineComment()) { 146 : // Do nothing. 147 : } 148 0 : _scanner.expect('*/'); 149 : 150 : return true; 151 : } 152 : }