From 6d60de76f4355de3d7963bafa71212b356e78070 Mon Sep 17 00:00:00 2001
From: Natalie Weizenbaum <nweiz@google.com>
Date: Thu, 18 Feb 2016 16:29:33 -0800
Subject: [PATCH] Make PlatformSelector use boolean_selector.

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//1717483002 .
---
 README.md                                     |  17 +-
 lib/src/backend/metadata.dart                 |   2 +-
 lib/src/backend/platform_selector.dart        |  98 +++----
 lib/src/backend/platform_selector/ast.dart    | 137 ---------
 .../backend/platform_selector/evaluator.dart  |  47 ----
 lib/src/backend/platform_selector/parser.dart | 105 -------
 .../backend/platform_selector/scanner.dart    | 146 ----------
 lib/src/backend/platform_selector/token.dart  |  70 -----
 .../backend/platform_selector/visitor.dart    |  44 ---
 pubspec.yaml                                  |   1 +
 test/backend/platform_selector/ast_test.dart  |  82 ------
 .../platform_selector/evaluate_test.dart      | 142 ----------
 .../platform_selector/parser_test.dart        | 266 ------------------
 .../platform_selector/scanner_test.dart       | 266 ------------------
 14 files changed, 45 insertions(+), 1378 deletions(-)
 delete mode 100644 lib/src/backend/platform_selector/ast.dart
 delete mode 100644 lib/src/backend/platform_selector/evaluator.dart
 delete mode 100644 lib/src/backend/platform_selector/parser.dart
 delete mode 100644 lib/src/backend/platform_selector/scanner.dart
 delete mode 100644 lib/src/backend/platform_selector/token.dart
 delete mode 100644 lib/src/backend/platform_selector/visitor.dart
 delete mode 100644 test/backend/platform_selector/ast_test.dart
 delete mode 100644 test/backend/platform_selector/evaluate_test.dart
 delete mode 100644 test/backend/platform_selector/parser_test.dart
 delete mode 100644 test/backend/platform_selector/scanner_test.dart

diff --git a/README.md b/README.md
index 13381113..6035cb91 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 * [Writing Tests](#writing-tests)
 * [Running Tests](#running-tests)
   * [Restricting Tests to Certain Platforms](#restricting-tests-to-certain-platforms)
-  * [Platform Selector Syntax](#platform-selector-syntax)
+  * [Platform Selectors](#platform-selectors)
   * [Running Tests on Dartium](#running-tests-on-dartium)
 * [Asynchronous Tests](#asynchronous-tests)
 * [Running Tests With Custom HTML](#running-tests-with-custom-html)
@@ -176,13 +176,16 @@ specifies exactly which platforms a test can run on. It can be as simple as the
 name of a platform, or a more complex Dart-like boolean expression involving
 these platform names.
 
-### Platform Selector Syntax
+### Platform Selectors
 
-Platform selectors can contain identifiers, parentheses, and operators. When
-loading a test, each identifier is set to `true` or `false` based on the current
-platform, and the test is only loaded if the platform selector returns `true`.
-The operators `||`, `&&`, `!`, and `? :` all work just like they do in Dart. The
-valid identifiers are:
+Platform selectors use the [boolean selector syntax][] defined in the
+[`boolean_selector` package][boolean_selector], which is a subset of Dart's
+expression syntax that only supports boolean operations. The following
+identifiers are defined:
+
+[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md
+
+[boolean_selector]: https://pub.dartlang.org/packages/boolean_selector
 
 * `vm`: Whether the test is running on the command-line Dart VM.
 
diff --git a/lib/src/backend/metadata.dart b/lib/src/backend/metadata.dart
index 6a5133c4..54eb64ed 100644
--- a/lib/src/backend/metadata.dart
+++ b/lib/src/backend/metadata.dart
@@ -252,7 +252,7 @@ class Metadata {
   /// is merged as well.
   Metadata merge(Metadata other) =>
       new Metadata(
-          testOn: testOn.intersect(other.testOn),
+          testOn: testOn.intersection(other.testOn),
           timeout: timeout.merge(other.timeout),
           skip: skip || other.skip,
           skipReason: other.skipReason == null ? skipReason : other.skipReason,
diff --git a/lib/src/backend/platform_selector.dart b/lib/src/backend/platform_selector.dart
index 841fbdc7..47adc1f9 100644
--- a/lib/src/backend/platform_selector.dart
+++ b/lib/src/backend/platform_selector.dart
@@ -2,13 +2,10 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:boolean_selector/boolean_selector.dart';
 import 'package:source_span/source_span.dart';
 
 import 'operating_system.dart';
-import 'platform_selector/ast.dart';
-import 'platform_selector/evaluator.dart';
-import 'platform_selector/parser.dart';
-import 'platform_selector/visitor.dart';
 import 'test_platform.dart';
 
 /// The set of all valid variable names.
@@ -20,82 +17,53 @@ final _validVariables =
 /// An expression for selecting certain platforms, including operating systems
 /// and browsers.
 ///
-/// The syntax is mostly Dart's expression syntax restricted to boolean
-/// operations. See [the README][] for full details.
+/// This uses the [boolean selector][] syntax.
 ///
-/// [the README]: https://github.com/dart-lang/test/#platform-selector-syntax
-abstract class PlatformSelector {
+/// [boolean selector]: https://pub.dartlang.org/packages/boolean_selector
+class PlatformSelector {
   /// A selector that declares that a test can be run on all platforms.
-  ///
-  /// This isn't representable in the platform selector syntax but it is the
-  /// default selector.
-  static const all = const _AllPlatforms();
+  static const all = const PlatformSelector._(BooleanSelector.all);
+
+  /// The boolean selector used to implement this selector.
+  final BooleanSelector _inner;
 
   /// Parses [selector].
   ///
   /// This will throw a [SourceSpanFormatException] if the selector is
   /// malformed or if it uses an undefined variable.
-  factory PlatformSelector.parse(String selector) =>
-      new _PlatformSelector.parse(selector);
+  PlatformSelector.parse(String selector)
+      : _inner = new BooleanSelector.parse(selector) {
+    _inner.validate(_validVariables.contains);
+  }
+
+  const PlatformSelector._(this._inner);
 
   /// Returns whether the selector matches the given [platform] and [os].
   ///
   /// [os] defaults to [OperatingSystem.none].
-  bool evaluate(TestPlatform platform, {OperatingSystem os});
+  bool evaluate(TestPlatform platform, {OperatingSystem os}) {
+    os ??= OperatingSystem.none;
+
+    return _inner.evaluate((variable) {
+      if (variable == platform.identifier) return true;
+      if (variable == os.name) return true;
+      switch (variable) {
+        case "dart-vm": return platform.isDartVM;
+        case "browser": return platform.isBrowser;
+        case "js": return platform.isJS;
+        case "blink": return platform.isBlink;
+        case "posix": return os.isPosix;
+        default: return false;
+      }
+    });
+  }
 
   /// Returns a new [PlatformSelector] that matches only platforms matched by
   /// both [this] and [other].
-  PlatformSelector intersect(PlatformSelector other);
-}
-
-/// The concrete implementation of a [PlatformSelector] parsed from a string.
-///
-/// This is separate from [PlatformSelector] so that [_AllPlatforms] can
-/// implement [PlatformSelector] without having to implement private members.
-class _PlatformSelector implements PlatformSelector{
-  /// The parsed AST.
-  final Node _selector;
-
-  _PlatformSelector.parse(String selector)
-      : _selector = new Parser(selector).parse() {
-    _selector.accept(const _VariableValidator());
-  }
-
-  _PlatformSelector(this._selector);
-
-  bool evaluate(TestPlatform platform, {OperatingSystem os}) =>
-      _selector.accept(new Evaluator(platform, os: os));
-
-  PlatformSelector intersect(PlatformSelector other) {
+  PlatformSelector intersection(PlatformSelector other) {
     if (other == PlatformSelector.all) return this;
-    return new _PlatformSelector(new AndNode(
-        _selector, (other as _PlatformSelector)._selector));
+    return new PlatformSelector._(_inner.intersection(other._inner));
   }
 
-  String toString() => _selector.toString();
-}
-
-/// A selector that matches all platforms.
-class _AllPlatforms implements PlatformSelector {
-  const _AllPlatforms();
-
-  bool evaluate(TestPlatform platform, {OperatingSystem os}) => true;
-
-  PlatformSelector intersect(PlatformSelector other) => other;
-
-  String toString() => "*";
-}
-
-/// An AST visitor that ensures that all variables are valid.
-///
-/// This isn't done when evaluating to ensure that errors are eagerly detected,
-/// and it isn't done when parsing to avoid coupling the syntax too tightly to
-/// the semantics.
-class _VariableValidator extends RecursiveVisitor {
-  const _VariableValidator();
-
-  void visitVariable(VariableNode node) {
-    if (_validVariables.contains(node.name)) return;
-    throw new SourceSpanFormatException("Undefined variable.", node.span);
-  }
+  String toString() => _inner.toString();
 }
diff --git a/lib/src/backend/platform_selector/ast.dart b/lib/src/backend/platform_selector/ast.dart
deleted file mode 100644
index 981514f3..00000000
--- a/lib/src/backend/platform_selector/ast.dart
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:source_span/source_span.dart';
-
-import 'visitor.dart';
-
-/// The superclass of nodes in the platform selector abstract syntax tree.
-abstract class Node {
-  /// The span indicating where this node came from.
-  ///
-  /// This is a [FileSpan] because the nodes are parsed from a single continuous
-  /// string, but the string itself isn't actually a file. It might come from a
-  /// statically-parsed annotation or from a parameter.
-  ///
-  /// This may be `null` for nodes without source information.
-  FileSpan get span;
-
-  /// Calls the appropriate [Visitor] method on [this] and returns the result.
-  accept(Visitor visitor);
-}
-
-/// A single variable.
-class VariableNode implements Node {
-  final FileSpan span;
-
-  /// The variable name.
-  final String name;
-
-  VariableNode(this.name, [this.span]);
-
-  accept(Visitor visitor) => visitor.visitVariable(this);
-
-  String toString() => name;
-}
-
-/// A negation expression.
-class NotNode implements Node {
-  final FileSpan span;
-
-  /// The expression being negated.
-  final Node child;
-
-  NotNode(this.child, [this.span]);
-
-  accept(Visitor visitor) => visitor.visitNot(this);
-
-  String toString() => child is VariableNode || child is NotNode
-      ? "!$child"
-      : "!($child)";
-}
-
-/// An or expression.
-class OrNode implements Node {
-  FileSpan get span => _expandSafe(left.span, right.span);
-
-  /// The left-hand branch of the expression.
-  final Node left;
-
-  /// The right-hand branch of the expression.
-  final Node right;
-
-  OrNode(this.left, this.right);
-
-  accept(Visitor visitor) => visitor.visitOr(this);
-
-  String toString() {
-    var string1 = left is AndNode || left is ConditionalNode
-        ? "($left)"
-        : left;
-    var string2 = right is AndNode || right is ConditionalNode
-        ? "($right)"
-        : right;
-
-    return "$string1 || $string2";
-  }
-}
-
-/// An and expression.
-class AndNode implements Node {
-  FileSpan get span => _expandSafe(left.span, right.span);
-
-  /// The left-hand branch of the expression.
-  final Node left;
-
-  /// The right-hand branch of the expression.
-  final Node right;
-
-  AndNode(this.left, this.right);
-
-  accept(Visitor visitor) => visitor.visitAnd(this);
-
-  String toString() {
-    var string1 = left is OrNode || left is ConditionalNode
-        ? "($left)"
-        : left;
-    var string2 = right is OrNode || right is ConditionalNode
-        ? "($right)"
-        : right;
-
-    return "$string1 && $string2";
-  }
-}
-
-/// A ternary conditional expression.
-class ConditionalNode implements Node {
-  FileSpan get span => _expandSafe(condition.span, whenFalse.span);
-
-  /// The condition expression to check.
-  final Node condition;
-
-  /// The branch to run if the condition is true.
-  final Node whenTrue;
-
-  /// The branch to run if the condition is false.
-  final Node whenFalse;
-
-  ConditionalNode(this.condition, this.whenTrue, this.whenFalse);
-
-  accept(Visitor visitor) => visitor.visitConditional(this);
-
-  String toString() {
-    var conditionString =
-        condition is ConditionalNode ? "($condition)" : condition;
-    var trueString = whenTrue is ConditionalNode ? "($whenTrue)" : whenTrue;
-    return "$conditionString ? $trueString : $whenFalse";
-  }
-}
-
-/// Like [FileSpan.expand], except if [start] and [end] are `null` or from
-/// different files it returns `null` rather than throwing an error.
-FileSpan _expandSafe(FileSpan start, FileSpan end) {
-  if (start == null || end == null) return null;
-  if (start.file != end.file) return null;
-  return start.expand(end);
-}
diff --git a/lib/src/backend/platform_selector/evaluator.dart b/lib/src/backend/platform_selector/evaluator.dart
deleted file mode 100644
index 9d04d6dc..00000000
--- a/lib/src/backend/platform_selector/evaluator.dart
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import '../operating_system.dart';
-import '../test_platform.dart';
-import 'ast.dart';
-import 'visitor.dart';
-
-/// A visitor for evaluating platform selectors against a specific
-/// [TestPlatform] and [OperatingSystem].
-class Evaluator implements Visitor<bool> {
-  /// The platform to test against.
-  final TestPlatform _platform;
-
-  /// The operating system to test against.
-  final OperatingSystem _os;
-
-  Evaluator(this._platform, {OperatingSystem os})
-      : _os = os == null ? OperatingSystem.none : os;
-
-  bool visitVariable(VariableNode node) {
-    if (node.name == _platform.identifier) return true;
-    if (node.name == _os.name) return true;
-
-    switch (node.name) {
-      case "dart-vm": return _platform.isDartVM;
-      case "browser": return _platform.isBrowser;
-      case "js": return _platform.isJS;
-      case "blink": return _platform.isBlink;
-      case "posix": return _os.isPosix;
-      default: return false;
-    }
-  }
-
-  bool visitNot(NotNode node) => !node.child.accept(this);
-
-  bool visitOr(OrNode node) =>
-      node.left.accept(this) || node.right.accept(this);
-
-  bool visitAnd(AndNode node) =>
-      node.left.accept(this) && node.right.accept(this);
-
-  bool visitConditional(ConditionalNode node) => node.condition.accept(this)
-      ? node.whenTrue.accept(this)
-      : node.whenFalse.accept(this);
-}
diff --git a/lib/src/backend/platform_selector/parser.dart b/lib/src/backend/platform_selector/parser.dart
deleted file mode 100644
index 4a08025a..00000000
--- a/lib/src/backend/platform_selector/parser.dart
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:source_span/source_span.dart';
-
-import 'ast.dart';
-import 'scanner.dart';
-import 'token.dart';
-
-/// A class for parsing a platform selector.
-///
-/// Platform selectors use a stripped-down version of the Dart expression
-/// syntax that only contains variables, parentheses, and boolean operators.
-/// Variables may also contain dashes, contrary to Dart's syntax; this allows
-/// consistency with command-line arguments.
-class Parser {
-  /// The scanner that tokenizes the selector.
-  final Scanner _scanner;
-
-  Parser(String selector)
-      : _scanner = new Scanner(selector);
-
-  /// Parses the selector.
-  ///
-  /// This must only be called once per parser.
-  Node parse() {
-    var selector = _conditional();
-
-    if (_scanner.peek().type != TokenType.endOfFile) {
-      throw new SourceSpanFormatException(
-          "Expected end of input.", _scanner.peek().span);
-    }
-
-    return selector;
-  }
-
-  /// Parses a conditional:
-  ///
-  ///     conditionalExpression:
-  ///       logicalOrExpression ("?" conditionalExpression ":"
-  ///           conditionalExpression)?
-  Node _conditional() {
-    var condition = _or();
-    if (!_scanner.scan(TokenType.questionMark)) return condition;
-
-    var whenTrue = _conditional();
-    if (!_scanner.scan(TokenType.colon)) {
-      throw new SourceSpanFormatException(
-          'Expected ":".', _scanner.peek().span);
-    }
-
-    var whenFalse = _conditional();
-    return new ConditionalNode(condition, whenTrue, whenFalse);
-  }
-
-  /// Parses a logical or:
-  ///
-  ///     logicalOrExpression:
-  ///       logicalAndExpression ("||" logicalOrExpression)?
-  Node _or() {
-    var left = _and();
-    if (!_scanner.scan(TokenType.or)) return left;
-    return new OrNode(left, _or());
-  }
-
-  /// Parses a logical and:
-  ///
-  ///     logicalAndExpression:
-  ///       simpleExpression ("&&" logicalAndExpression)?
-  Node _and() {
-    var left = _simpleExpression();
-    if (!_scanner.scan(TokenType.and)) return left;
-    return new AndNode(left, _and());
-  }
-
-  /// Parses a simple expression:
-  ///
-  ///     simpleExpression:
-  ///       "!" simpleExpression |
-  ///           "(" conditionalExpression ")" |
-  ///           IDENTIFIER
-  Node _simpleExpression() {
-    var token = _scanner.next();
-    switch (token.type) {
-      case TokenType.not:
-        var child = _simpleExpression();
-        return new NotNode(child, token.span.expand(child.span));
-
-      case TokenType.leftParen:
-        var child = _conditional();
-        if (!_scanner.scan(TokenType.rightParen)) {
-          throw new SourceSpanFormatException(
-              'Expected ")".', _scanner.peek().span);
-        }
-        return child;
-
-      case TokenType.identifier:
-        return new VariableNode(token.name, token.span);
-
-      default:
-        throw new SourceSpanFormatException("Expected expression.", token.span);
-    }
-  }
-}
diff --git a/lib/src/backend/platform_selector/scanner.dart b/lib/src/backend/platform_selector/scanner.dart
deleted file mode 100644
index 44dddca4..00000000
--- a/lib/src/backend/platform_selector/scanner.dart
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:string_scanner/string_scanner.dart';
-
-import '../../utils.dart';
-import 'token.dart';
-
-/// A regular expression matching both whitespace and single-line comments.
-///
-/// This will only match if consumes at least one character.
-final _whitespaceAndSingleLineComments =
-    new RegExp(r"([ \t\n]+|//[^\n]*(\n|$))+");
-
-/// A regular expression matching the body of a multi-line comment, after `/*`
-/// but before `*/` or a nested `/*`.
-///
-/// This will only match if it consumes at least one character.
-final _multiLineCommentBody = new RegExp(r"([^/*]|/[^*]|\*[^/])+");
-
-/// A scanner that converts a platform selector string into a stream of
-/// tokens.
-class Scanner {
-  /// The underlying string scanner.
-  final SpanScanner _scanner;
-
-  /// The next token to emit.
-  Token _next;
-
-  /// Whether the scanner has emitted a [TokenType.endOfFile] token.
-  bool _endOfFileEmitted = false;
-
-  Scanner(String selector)
-      : _scanner = new SpanScanner(selector);
-
-  /// Returns the next token that will be returned by [next].
-  ///
-  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
-  /// consumed.
-  Token peek() {
-    if (_next == null) _next = _getNext();
-    return _next;
-  }
-
-  /// Consumes and returns the next token in the stream.
-  ///
-  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
-  /// consumed.
-  Token next() {
-    var token = _next == null ? _getNext() : _next;
-    _endOfFileEmitted = token.type == TokenType.endOfFile;
-    _next = null;
-    return token;
-  }
-
-  /// If the next token matches [type], consumes it and returns `true`;
-  /// otherwise, returns `false`.
-  ///
-  /// Throws a [StateError] if a [TokenType.endOfFile] token has already been
-  /// consumed.
-  bool scan(TokenType type) {
-    if (peek().type != type) return false;
-    next();
-    return true;
-  }
-
-  /// Scan and return the next token in the stream.
-  Token _getNext() {
-    if (_endOfFileEmitted) throw new StateError("No more tokens.");
-
-    _consumeWhitespace();
-    if (_scanner.isDone) {
-      return new Token(
-          TokenType.endOfFile, _scanner.spanFrom(_scanner.state));
-    }
-
-    switch (_scanner.peekChar()) {
-      case 0x28 /* ( */: return _scanOperator(TokenType.leftParen);
-      case 0x29 /* ) */: return _scanOperator(TokenType.rightParen);
-      case 0x3F /* ? */: return _scanOperator(TokenType.questionMark);
-      case 0x3A /* : */: return _scanOperator(TokenType.colon);
-      case 0x21 /* ! */: return _scanOperator(TokenType.not);
-      case 0x7C /* | */: return _scanOr();
-      case 0x26 /* & */: return _scanAnd();
-      default: return _scanIdentifier();
-    }
-  }
-
-  /// Scans a single-character operator and returns a token of type [type].
-  ///
-  /// This assumes that the caller has already verified that the next character
-  /// is correct for the given operator.
-  Token _scanOperator(TokenType type) {
-    var start = _scanner.state;
-    _scanner.readChar();
-    return new Token(type, _scanner.spanFrom(start));
-  }
-
-  /// Scans a `||` operator and returns the appropriate token.
-  ///
-  /// This validates that the next two characters are `||`.
-  Token _scanOr() {
-    var start = _scanner.state;
-    _scanner.expect("||");
-    return new Token(TokenType.or, _scanner.spanFrom(start));
-  }
-
-  /// Scans a `&&` operator and returns the appropriate token.
-  ///
-  /// This validates that the next two characters are `&&`.
-  Token _scanAnd() {
-    var start = _scanner.state;
-    _scanner.expect("&&");
-    return new Token(TokenType.and, _scanner.spanFrom(start));
-  }
-
-  /// Scans and returns an identifier token.
-  Token _scanIdentifier() {
-    _scanner.expect(hyphenatedIdentifier, name: "expression");
-    return new IdentifierToken(_scanner.lastMatch[0], _scanner.lastSpan);
-  }
-
-  /// Consumes all whitespace and comments immediately following the cursor's
-  /// current position.
-  void _consumeWhitespace() {
-    while (_scanner.scan(_whitespaceAndSingleLineComments) ||
-        _multiLineComment()) {
-      // Do nothing.
-    }
-  }
-
-  /// Consumes a single multi-line comment.
-  ///
-  /// Returns whether or not a comment was consumed.
-  bool _multiLineComment() {
-    if (!_scanner.scan("/*")) return false;
-
-    while (_scanner.scan(_multiLineCommentBody) || _multiLineComment()) {
-      // Do nothing.
-    }
-    _scanner.expect("*/");
-
-    return true;
-  }
-}
diff --git a/lib/src/backend/platform_selector/token.dart b/lib/src/backend/platform_selector/token.dart
deleted file mode 100644
index 759999f5..00000000
--- a/lib/src/backend/platform_selector/token.dart
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:source_span/source_span.dart';
-
-/// A token in a platform selector.
-class Token {
-  /// The type of the token.
-  final TokenType type;
-
-  /// The span indicating where this token came from.
-  ///
-  /// This is a [FileSpan] because the tokens are parsed from a single
-  /// continuous string, but the string itself isn't actually a file. It might
-  /// come from a statically-parsed annotation or from a parameter.
-  final FileSpan span;
-
-  Token(this.type, this.span);
-}
-
-/// A token representing an identifier.
-class IdentifierToken implements Token {
-  final type = TokenType.identifier;
-  final FileSpan span;
-
-  /// The name of the identifier.
-  final String name;
-
-  IdentifierToken(this.name, this.span);
-
-  String toString() => 'identifier "$name"';
-}
-
-/// An enumeration of types of tokens.
-class TokenType {
-  /// A `(` character.
-  static const leftParen = const TokenType._("left paren");
-
-  /// A `)` character.
-  static const rightParen = const TokenType._("right paren");
-
-  /// A `||` sequence.
-  static const or = const TokenType._("or");
-
-  /// A `&&` sequence.
-  static const and = const TokenType._("and");
-
-  /// A `!` character.
-  static const not = const TokenType._("not");
-
-  /// A `?` character.
-  static const questionMark = const TokenType._("question mark");
-
-  /// A `:` character.
-  static const colon = const TokenType._("colon");
-
-  /// A named identifier.
-  static const identifier = const TokenType._("identifier");
-
-  /// The end of the selector.
-  static const endOfFile = const TokenType._("end of file");
-
-  /// The name of the token type.
-  final String name;
-
-  const TokenType._(this.name);
-
-  String toString() => name;
-}
diff --git a/lib/src/backend/platform_selector/visitor.dart b/lib/src/backend/platform_selector/visitor.dart
deleted file mode 100644
index 3687f1d8..00000000
--- a/lib/src/backend/platform_selector/visitor.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'ast.dart';
-
-/// The interface for visitors of the platform selector AST.
-abstract class Visitor<T> {
-  T visitVariable(VariableNode node);
-  T visitNot(NotNode node);
-  T visitOr(OrNode node);
-  T visitAnd(AndNode node);
-  T visitConditional(ConditionalNode node);
-}
-
-/// An abstract superclass for side-effect-based visitors.
-///
-/// The default implementations of this visitor's methods just traverse the AST
-/// and do nothing with it.
-abstract class RecursiveVisitor implements Visitor {
-  const RecursiveVisitor();
-
-  void visitVariable(VariableNode node) {}
-
-  void visitNot(NotNode node) {
-    node.child.accept(this);
-  }
-
-  void visitOr(OrNode node) {
-    node.left.accept(this);
-    node.right.accept(this);
-  }
-
-  void visitAnd(AndNode node) {
-    node.left.accept(this);
-    node.right.accept(this);
-  }
-
-  void visitConditional(ConditionalNode node) {
-    node.condition.accept(this);
-    node.whenTrue.accept(this);
-    node.whenFalse.accept(this);
-  }
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index c2fed2ec..a4e6781e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,6 +10,7 @@ dependencies:
   args: '^0.13.1'
   async: '^1.8.0'
   barback: '>=0.14.0 <0.16.0'
+  boolean_selector: '^1.0.0'
   collection: '^1.1.0'
   crypto: '^0.9.0'
   glob: '^1.0.0'
diff --git a/test/backend/platform_selector/ast_test.dart b/test/backend/platform_selector/ast_test.dart
deleted file mode 100644
index 876f7dc9..00000000
--- a/test/backend/platform_selector/ast_test.dart
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:test/test.dart';
-import 'package:test/src/backend/platform_selector/parser.dart';
-
-void main() {
-  group("toString() for", () {
-    test("a variable is its name", () {
-      _expectToString("foo");
-      _expectToString("a-b");
-    });
-
-    group("not", () {
-      test("doesn't parenthesize a variable", () => _expectToString("!a"));
-      test("doesn't parenthesize a nested not", () => _expectToString("!!a"));
-      test("parenthesizes an or", () => _expectToString("!(a || b)"));
-      test("parenthesizes an and", () => _expectToString("!(a && b)"));
-      test("parenthesizes a condition", () => _expectToString("!(a ? b : c)"));
-    });
-
-    group("or", () {
-      test("doesn't parenthesize variables", () => _expectToString("a || b"));
-      test("doesn't parenthesize nots", () => _expectToString("!a || !b"));
-
-      test("doesn't parenthesize ors", () {
-        _expectToString("a || b || c || d");
-        _expectToString("((a || b) || c) || d", "a || b || c || d");
-      });
-
-      test("parenthesizes ands", () =>
-          _expectToString("a && b || c && d", "(a && b) || (c && d)"));
-
-      test("parenthesizes conditions", () =>
-          _expectToString("(a ? b : c) || (e ? f : g)"));
-    });
-
-    group("and", () {
-      test("doesn't parenthesize variables", () => _expectToString("a && b"));
-      test("doesn't parenthesize nots", () => _expectToString("!a && !b"));
-
-      test("parenthesizes ors", () =>
-          _expectToString("(a || b) && (c || d)", "(a || b) && (c || d)"));
-
-      test("doesn't parenthesize ands", () {
-        _expectToString("a && b && c && d");
-        _expectToString("((a && b) && c) && d", "a && b && c && d");
-      });
-
-      test("parenthesizes conditions", () =>
-          _expectToString("(a ? b : c) && (e ? f : g)"));
-    });
-
-    group("conditional", () {
-      test("doesn't parenthesize variables", () =>
-          _expectToString("a ? b : c"));
-
-      test("doesn't parenthesize nots", () => _expectToString("!a ? !b : !c"));
-
-      test("doesn't parenthesize ors", () =>
-          _expectToString("a || b ? c || d : e || f"));
-
-      test("doesn't parenthesize ands", () =>
-          _expectToString("a && b ? c && d : e && f"));
-
-      test("parenthesizes non-trailing conditions", () {
-        _expectToString("(a ? b : c) ? (e ? f : g) : h ? i : j");
-        _expectToString("(a ? b : c) ? (e ? f : g) : (h ? i : j)",
-            "(a ? b : c) ? (e ? f : g) : h ? i : j");
-      });
-    });
-  });
-}
-
-void _expectToString(String selector, [String result]) {
-  if (result == null) result = selector;
-  expect(_toString(selector), equals(result),
-      reason: 'Expected toString of "$selector" to be "$result".');
-}
-
-String _toString(String selector) => new Parser(selector).parse().toString();
diff --git a/test/backend/platform_selector/evaluate_test.dart b/test/backend/platform_selector/evaluate_test.dart
deleted file mode 100644
index 4f81d1b9..00000000
--- a/test/backend/platform_selector/evaluate_test.dart
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-@TestOn("vm")
-
-import 'dart:io';
-
-import 'package:test/test.dart';
-import 'package:test/src/backend/operating_system.dart';
-import 'package:test/src/backend/platform_selector.dart';
-import 'package:test/src/backend/test_platform.dart';
-
-void main() {
-  test("new PlatformSelector.parse() disallows invalid variables", () {
-    expect(() => new PlatformSelector.parse("undefined"),
-        throwsFormatException);
-  });
-
-  group("operator:", () {
-    test("conditional", () {
-      _expectEval("vm ? vm : browser", true);
-      _expectEval("vm ? browser : vm", false);
-      _expectEval("browser ? vm : browser", false);
-      _expectEval("browser ? browser : vm", true);
-    });
-
-    test("or", () {
-      _expectEval("vm || vm", true);
-      _expectEval("vm || browser", true);
-      _expectEval("browser || vm", true);
-      _expectEval("browser || browser", false);
-    });
-
-    test("and", () {
-      _expectEval("vm && vm", true);
-      _expectEval("vm && browser", false);
-      _expectEval("browser && vm", false);
-      _expectEval("browser && browser", false);
-    });
-
-    test("not", () {
-      _expectEval("!vm", false);
-      _expectEval("!browser", true);
-    });
-  });
-
-  group("baseline variable:", () {
-    test("vm", () {
-      _expectEval("vm", true, platform: TestPlatform.vm);
-      _expectEval("vm", false, platform: TestPlatform.chrome);
-    });
-
-    test("chrome", () {
-      _expectEval("chrome", true, platform: TestPlatform.chrome);
-      _expectEval("chrome", false, platform: TestPlatform.vm);
-    });
-
-    test("windows", () {
-      _expectEval("windows", true, os: OperatingSystem.windows);
-      _expectEval("windows", false, os: OperatingSystem.linux);
-      _expectEval("windows", false, os: OperatingSystem.none);
-    });
-
-    test("mac-os", () {
-      _expectEval("mac-os", true, os: OperatingSystem.macOS);
-      _expectEval("mac-os", false, os: OperatingSystem.linux);
-      _expectEval("mac-os", false, os: OperatingSystem.none);
-    });
-
-    test("linux", () {
-      _expectEval("linux", true, os: OperatingSystem.linux);
-      _expectEval("linux", false, os: OperatingSystem.android);
-      _expectEval("linux", false, os: OperatingSystem.none);
-    });
-
-    test("android", () {
-      _expectEval("android", true, os: OperatingSystem.android);
-      _expectEval("android", false, os: OperatingSystem.linux);
-      _expectEval("android", false, os: OperatingSystem.none);
-    });
-  });
-
-  group("derived variable:", () {
-    test("dart-vm", () {
-      _expectEval("dart-vm", true, platform: TestPlatform.vm);
-      _expectEval("dart-vm", false, platform: TestPlatform.chrome);
-    });
-
-    test("browser", () {
-      _expectEval("browser", true, platform: TestPlatform.chrome);
-      _expectEval("browser", false, platform: TestPlatform.vm);
-    });
-
-    test("js", () {
-      _expectEval("js", true, platform: TestPlatform.chrome);
-      _expectEval("js", false, platform: TestPlatform.vm);
-    });
-
-    test("blink", () {
-      _expectEval("blink", true, platform: TestPlatform.chrome);
-      _expectEval("blink", false, platform: TestPlatform.vm);
-    });
-
-    test("posix", () {
-      _expectEval("posix", false, os: OperatingSystem.windows);
-      _expectEval("posix", true, os: OperatingSystem.macOS);
-      _expectEval("posix", true, os: OperatingSystem.linux);
-      _expectEval("posix", true, os: OperatingSystem.android);
-      _expectEval("posix", false, os: OperatingSystem.none);
-    });
-  });
-}
-
-/// Asserts that [expression] evaluates to [result] on [platform] and [os].
-///
-/// [platform] defaults to [TestPlatform.vm]; [os] defaults to the current
-/// operating system.
-void _expectEval(String expression, bool result, {TestPlatform platform,
-    OperatingSystem os}) {
-
-  var reason = 'Expected "$expression" to evaluate to $result';
-  if (platform != null && os != null) {
-    reason += ' on $platform and $os.';
-  } else if (platform != null || os != null) {
-    reason += ' on ${platform == null ? os : platform}';
-  }
-
-  expect(_eval(expression, platform: platform, os: os), equals(result),
-      reason: '$reason.');
-}
-
-/// Returns the result of evaluating [expression] on [platform] and [os].
-///
-/// [platform] defaults to [TestPlatform.vm]; [os] defaults to the current
-/// operating system.
-bool _eval(String expression, {TestPlatform platform, OperatingSystem os}) {
-  if (platform == null) platform = TestPlatform.vm;
-  if (os == null) os = OperatingSystem.findByIoName(Platform.operatingSystem);
-  var selector = new PlatformSelector.parse(expression);
-  return selector.evaluate(platform, os: os);
-}
diff --git a/test/backend/platform_selector/parser_test.dart b/test/backend/platform_selector/parser_test.dart
deleted file mode 100644
index 04f356c6..00000000
--- a/test/backend/platform_selector/parser_test.dart
+++ /dev/null
@@ -1,266 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:test/test.dart';
-import 'package:test/src/backend/platform_selector/ast.dart';
-import 'package:test/src/backend/platform_selector/parser.dart';
-
-/// A matcher that asserts that a value is a [ConditionalNode].
-Matcher _isConditionalNode = new isInstanceOf<ConditionalNode>();
-
-/// A matcher that asserts that a value is an [OrNode].
-Matcher _isOrNode = new isInstanceOf<OrNode>();
-
-/// A matcher that asserts that a value is an [AndNode].
-Matcher _isAndNode = new isInstanceOf<AndNode>();
-
-/// A matcher that asserts that a value is a [NotNode].
-Matcher _isNotNode = new isInstanceOf<NotNode>();
-
-void main() {
-  group("parses a conditional expression", () {
-    test("with identifiers", () {
-      var node = _parse("  a ? b : c   ");
-      expect(node.toString(), equals("a ? b : c"));
-
-      expect(node.span.text, equals("a ? b : c"));
-      expect(node.span.start.offset, equals(2));
-      expect(node.span.end.offset, equals(11));
-    });
-
-    test("with nested ors", () {
-      // Should parse as "(a || b) ? (c || d) : (e || f)".
-      // Should not parse as "a || (b ? (c || d) : (e || f))".
-      // Should not parse as "((a || b) ? (c || d) : e) || f".
-      // Should not parse as "a || (b ? (c || d) : e) || f".
-      _expectToString("a || b ? c || d : e || f",
-          "a || b ? c || d : e || f");
-    });
-
-    test("with a conditional expression as branch 1", () {
-      // Should parse as "a ? (b ? c : d) : e".
-      var node = _parse("a ? b ? c : d : e");
-      expect(node, _isConditionalNode);
-      expect(node.condition, _isVar("a"));
-      expect(node.whenFalse, _isVar("e"));
-
-      expect(node.whenTrue, _isConditionalNode);
-      expect(node.whenTrue.condition, _isVar("b"));
-      expect(node.whenTrue.whenTrue, _isVar("c"));
-      expect(node.whenTrue.whenFalse, _isVar("d"));
-    });
-
-    test("with a conditional expression as branch 2", () {
-      // Should parse as "a ? b : (c ? d : e)".
-      // Should not parse as "(a ? b : c) ? d : e".
-      var node = _parse("a ? b : c ? d : e");
-      expect(node, _isConditionalNode);
-      expect(node.condition, _isVar("a"));
-      expect(node.whenTrue, _isVar("b"));
-
-      expect(node.whenFalse, _isConditionalNode);
-      expect(node.whenFalse.condition, _isVar("c"));
-      expect(node.whenFalse.whenTrue, _isVar("d"));
-      expect(node.whenFalse.whenFalse, _isVar("e"));
-    });
-
-    group("which must have", () {
-      test("an expression after the ?", () {
-        expect(() => _parse("a ?"), throwsFormatException);
-        expect(() => _parse("a ? && b"), throwsFormatException);
-      });
-
-      test("a :", () {
-        expect(() => _parse("a ? b"), throwsFormatException);
-        expect(() => _parse("a ? b && c"), throwsFormatException);
-      });
-
-      test("an expression after the :", () {
-        expect(() => _parse("a ? b :"), throwsFormatException);
-        expect(() => _parse("a ? b : && c"), throwsFormatException);
-      });
-    });
-  });
-
-  group("parses an or expression", () {
-    test("with identifiers", () {
-      var node = _parse("  a || b   ");
-      expect(node, _isOrNode);
-      expect(node.left, _isVar("a"));
-      expect(node.right, _isVar("b"));
-
-      expect(node.span.text, equals("a || b"));
-      expect(node.span.start.offset, equals(2));
-      expect(node.span.end.offset, equals(8));
-    });
-
-    test("with nested ands", () {
-      // Should parse as "(a && b) || (c && d)".
-      // Should not parse as "a && (b || c) && d".
-      var node = _parse("a && b || c && d");
-      expect(node, _isOrNode);
-
-      expect(node.left, _isAndNode);
-      expect(node.left.left, _isVar("a"));
-      expect(node.left.right, _isVar("b"));
-
-      expect(node.right, _isAndNode);
-      expect(node.right.left, _isVar("c"));
-      expect(node.right.right, _isVar("d"));
-    });
-
-    test("with trailing ors", () {
-      // Should parse as "a || (b || (c || d))", although it doesn't affect the
-      // semantics.
-      var node = _parse("a || b || c || d");
-
-      for (var variable in ["a", "b", "c"]) {
-        expect(node, _isOrNode);
-        expect(node.left, _isVar(variable));
-        node = node.right;
-      }
-      expect(node, _isVar("d"));
-    });
-
-    test("which must have an expression after the ||", () {
-      expect(() => _parse("a ||"), throwsFormatException);
-      expect(() => _parse("a || && b"), throwsFormatException);
-    });
-  });
-
-  group("parses an and expression", () {
-    test("with identifiers", () {
-      var node = _parse("  a && b   ");
-      expect(node, _isAndNode);
-      expect(node.left, _isVar("a"));
-      expect(node.right, _isVar("b"));
-
-      expect(node.span.text, equals("a && b"));
-      expect(node.span.start.offset, equals(2));
-      expect(node.span.end.offset, equals(8));
-    });
-
-    test("with nested nots", () {
-      // Should parse as "(!a) && (!b)", obviously.
-      // Should not parse as "!(a && (!b))".
-      var node = _parse("!a && !b");
-      expect(node, _isAndNode);
-
-      expect(node.left, _isNotNode);
-      expect(node.left.child, _isVar("a"));
-
-      expect(node.right, _isNotNode);
-      expect(node.right.child, _isVar("b"));
-    });
-
-    test("with trailing ands", () {
-      // Should parse as "a && (b && (c && d))", although it doesn't affect the
-      // semantics since .
-      var node = _parse("a && b && c && d");
-
-      for (var variable in ["a", "b", "c"]) {
-        expect(node, _isAndNode);
-        expect(node.left, _isVar(variable));
-        node = node.right;
-      }
-      expect(node, _isVar("d"));
-    });
-
-    test("which must have an expression after the &&", () {
-      expect(() => _parse("a &&"), throwsFormatException);
-      expect(() => _parse("a && && b"), throwsFormatException);
-    });
-  });
-
-  group("parses a not expression", () {
-    test("with an identifier", () {
-      var node = _parse("  ! a    ");
-      expect(node, _isNotNode);
-      expect(node.child, _isVar("a"));
-
-      expect(node.span.text, equals("! a"));
-      expect(node.span.start.offset, equals(2));
-      expect(node.span.end.offset, equals(5));
-    });
-
-    test("with a parenthesized expression", () {
-      var node = _parse("!(a || b)");
-      expect(node, _isNotNode);
-
-      expect(node.child, _isOrNode);
-      expect(node.child.left, _isVar("a"));
-      expect(node.child.right, _isVar("b"));
-    });
-
-    test("with a nested not", () {
-      var node = _parse("!!a");
-      expect(node, _isNotNode);
-      expect(node.child, _isNotNode);
-      expect(node.child.child, _isVar("a"));
-    });
-
-    test("which must have an expression after the !", () {
-      expect(() => _parse("!"), throwsFormatException);
-      expect(() => _parse("! && a"), throwsFormatException);
-    });
-  });
-
-  group("parses a parenthesized expression", () {
-    test("with an identifier", () {
-      var node = _parse("(a)");
-      expect(node, _isVar("a"));
-    });
-
-    test("controls precedence", () {
-      // Without parentheses, this would parse as "(a || b) ? c : d".
-      var node = _parse("a || (b ? c : d)");
-
-      expect(node, _isOrNode);
-      expect(node.left, _isVar("a"));
-
-      expect(node.right, _isConditionalNode);
-      expect(node.right.condition, _isVar("b"));
-      expect(node.right.whenTrue, _isVar("c"));
-      expect(node.right.whenFalse, _isVar("d"));
-    });
-
-    group("which must have", () {
-      test("an expression within the ()", () {
-        expect(() => _parse("()"), throwsFormatException);
-        expect(() => _parse("( && a )"), throwsFormatException);
-      });
-
-      test("a matching )", () {
-        expect(() => _parse("( a"), throwsFormatException);
-      });
-    });
-  });
-
-  group("disallows", () {
-    test("an empty selector", () {
-      expect(() => _parse(""), throwsFormatException);
-    });
-
-    test("too many expressions", () {
-      expect(() => _parse("a b"), throwsFormatException);
-    });
-  });
-}
-
-/// Parses [selector] and returns its root node.
-Node _parse(String selector) => new Parser(selector).parse();
-
-/// A matcher that asserts that a value is a [VariableNode] with the given
-/// [name].
-Matcher _isVar(String name) => predicate(
-    (value) => value is VariableNode && value.name == name,
-    'is a variable named "$name"');
-
-void _expectToString(String selector, [String result]) {
-  if (result == null) result = selector;
-  expect(_toString(selector), equals(result),
-      reason: 'Expected toString of "$selector" to be "$result".');
-}
-
-String _toString(String selector) => new Parser(selector).parse().toString();
diff --git a/test/backend/platform_selector/scanner_test.dart b/test/backend/platform_selector/scanner_test.dart
deleted file mode 100644
index 6ba9aeb5..00000000
--- a/test/backend/platform_selector/scanner_test.dart
+++ /dev/null
@@ -1,266 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:test/test.dart';
-import 'package:test/src/backend/platform_selector/scanner.dart';
-import 'package:test/src/backend/platform_selector/token.dart';
-
-void main() {
-  group("peek()", () {
-    test("returns the next token without consuming it", () {
-      var scanner = new Scanner("( )");
-      expect(scanner.peek().type, equals(TokenType.leftParen));
-      expect(scanner.peek().type, equals(TokenType.leftParen));
-      expect(scanner.peek().type, equals(TokenType.leftParen));
-    });
-
-    test("returns an end-of-file token at the end of a file", () {
-      var scanner = new Scanner("( )");
-      scanner.next();
-      scanner.next();
-
-      var token = scanner.peek();
-      expect(token.type, equals(TokenType.endOfFile));
-      expect(token.span.start.offset, equals(3));
-      expect(token.span.end.offset, equals(3));
-    });
-
-    test("throws a StateError if called after end-of-file was consumed", () {
-      var scanner = new Scanner("( )");
-      scanner.next();
-      scanner.next();
-      scanner.next();
-      expect(() => scanner.peek(), throwsStateError);
-    });
-  });
-
-  group("next()", () {
-    test("consumes and returns the next token", () {
-      var scanner = new Scanner("( )");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.peek().type, equals(TokenType.rightParen));
-      expect(scanner.next().type, equals(TokenType.rightParen));
-    });
-
-    test("returns an end-of-file token at the end of a file", () {
-      var scanner = new Scanner("( )");
-      scanner.next();
-      scanner.next();
-
-      var token = scanner.next();
-      expect(token.type, equals(TokenType.endOfFile));
-      expect(token.span.start.offset, equals(3));
-      expect(token.span.end.offset, equals(3));
-    });
-
-    test("throws a StateError if called after end-of-file was consumed", () {
-      var scanner = new Scanner("( )");
-      scanner.next();
-      scanner.next();
-      scanner.next();
-      expect(() => scanner.next(), throwsStateError);
-    });
-  });
-
-  group("scan()", () {
-    test("consumes a matching token and returns true", () {
-      var scanner = new Scanner("( )");
-      expect(scanner.scan(TokenType.leftParen), isTrue);
-      expect(scanner.peek().type, equals(TokenType.rightParen));
-    });
-
-    test("doesn't consume a matching token and returns false", () {
-      var scanner = new Scanner("( )");
-      expect(scanner.scan(TokenType.questionMark), isFalse);
-      expect(scanner.peek().type, equals(TokenType.leftParen));
-    });
-
-    test("throws a StateError called after end-of-file was consumed", () {
-      var scanner = new Scanner("( )");
-      scanner.next();
-      scanner.next();
-      scanner.next();
-      expect(() => scanner.scan(TokenType.endOfFile), throwsStateError);
-    });
-  });
-
-  group("scans a simple token:", () {
-    test("left paren", () => _expectSimpleScan("(", TokenType.leftParen));
-    test("right paren", () => _expectSimpleScan(")", TokenType.rightParen));
-    test("or", () => _expectSimpleScan("||", TokenType.or));
-    test("and", () => _expectSimpleScan("&&", TokenType.and));
-    test("not", () => _expectSimpleScan("!", TokenType.not));
-    test("question mark", () => _expectSimpleScan("?", TokenType.questionMark));
-    test("colon", () => _expectSimpleScan(":", TokenType.colon));
-  });
-
-  group("scans an identifier that", () {
-    test("is simple", () {
-      var token = _scan("   foo  ");
-      expect(token.name, equals("foo"));
-      expect(token.span.text, equals("foo"));
-      expect(token.span.start.offset, equals(3));
-      expect(token.span.end.offset, equals(6));
-    });
-
-    test("is a single character", () {
-      var token = _scan("f");
-      expect(token.name, equals("f"));
-    });
-
-    test("has a leading underscore", () {
-      var token = _scan("_foo");
-      expect(token.name, equals("_foo"));
-    });
-
-    test("has a leading dash", () {
-      var token = _scan("-foo");
-      expect(token.name, equals("-foo"));
-    });
-
-    test("contains an underscore", () {
-      var token = _scan("foo_bar");
-      expect(token.name, equals("foo_bar"));
-    });
-
-    test("contains a dash", () {
-      var token = _scan("foo-bar");
-      expect(token.name, equals("foo-bar"));
-    });
-
-    test("is capitalized", () {
-      var token = _scan("FOO");
-      expect(token.name, equals("FOO"));
-    });
-
-    test("contains numbers", () {
-      var token = _scan("foo123");
-      expect(token.name, equals("foo123"));
-    });
-  });
-
-  test("scans an empty selector", () {
-    expect(_scan("").type, equals(TokenType.endOfFile));
-  });
-
-  test("scans multiple tokens", () {
-    var scanner = new Scanner("(foo && bar)");
-
-    var token = scanner.next();
-    expect(token.type, equals(TokenType.leftParen));
-    expect(token.span.start.offset, equals(0));
-    expect(token.span.end.offset, equals(1));
-
-    token = scanner.next();
-    expect(token.type, equals(TokenType.identifier));
-    expect(token.name, equals("foo"));
-    expect(token.span.start.offset, equals(1));
-    expect(token.span.end.offset, equals(4));
-
-    token = scanner.next();
-    expect(token.type, equals(TokenType.and));
-    expect(token.span.start.offset, equals(5));
-    expect(token.span.end.offset, equals(7));
-
-    token = scanner.next();
-    expect(token.type, equals(TokenType.identifier));
-    expect(token.name, equals("bar"));
-    expect(token.span.start.offset, equals(8));
-    expect(token.span.end.offset, equals(11));
-
-    token = scanner.next();
-    expect(token.type, equals(TokenType.rightParen));
-    expect(token.span.start.offset, equals(11));
-    expect(token.span.end.offset, equals(12));
-
-    token = scanner.next();
-    expect(token.type, equals(TokenType.endOfFile));
-    expect(token.span.start.offset, equals(12));
-    expect(token.span.end.offset, equals(12));
-  });
-
-  group("ignores", () {
-    test("a single-line comment", () {
-      var scanner = new Scanner("( // &&\n// ||\n)");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.next().type, equals(TokenType.rightParen));
-      expect(scanner.next().type, equals(TokenType.endOfFile));
-    });
-
-    test("a single-line comment without a trailing newline", () {
-      var scanner = new Scanner("( // &&");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.next().type, equals(TokenType.endOfFile));
-    });
-
-    test("a multi-line comment", () {
-      var scanner = new Scanner("( /* && * /\n|| */\n)");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.next().type, equals(TokenType.rightParen));
-      expect(scanner.next().type, equals(TokenType.endOfFile));
-    });
-
-    test("a multi-line nested comment", () {
-      var scanner = new Scanner("(/* && /* ? /* || */ : */ ! */)");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.next().type, equals(TokenType.rightParen));
-      expect(scanner.next().type, equals(TokenType.endOfFile));
-    });
-
-    test("Dart's notion of whitespace", () {
-      var scanner = new Scanner("( \t \n)");
-      expect(scanner.next().type, equals(TokenType.leftParen));
-      expect(scanner.next().type, equals(TokenType.rightParen));
-      expect(scanner.next().type, equals(TokenType.endOfFile));
-    });
-  });
-
-  group("disallows", () {
-    test("a single |", () {
-      expect(() => _scan("|"), throwsFormatException);
-    });
-
-    test('"| |"', () {
-      expect(() => _scan("| |"), throwsFormatException);
-    });
-
-    test("a single &", () {
-      expect(() => _scan("&"), throwsFormatException);
-    });
-
-    test('"& &"', () {
-      expect(() => _scan("& &"), throwsFormatException);
-    });
-
-    test("an unknown operator", () {
-      expect(() => _scan("=="), throwsFormatException);
-    });
-
-    test("unicode", () {
-      expect(() => _scan("öh"), throwsFormatException);
-    });
-
-    test("an unclosed multi-line comment", () {
-      expect(() => _scan("/*"), throwsFormatException);
-    });
-
-    test("an unopened multi-line comment", () {
-      expect(() => _scan("*/"), throwsFormatException);
-    });
-  });
-}
-
-/// Asserts that the first token scanned from [selector] has type [type],
-/// and that that token's span is exactly [selector].
-void _expectSimpleScan(String selector, TokenType type) {
-  // Complicate the selector to test that the span covers it correctly.
-  var token = _scan("   $selector  ");
-  expect(token.type, equals(type));
-  expect(token.span.text, equals(selector));
-  expect(token.span.start.offset, equals(3));
-  expect(token.span.end.offset, equals(3 + selector.length));
-}
-
-/// Scans a single token from [selector].
-Token _scan(String selector) => new Scanner(selector).next();
-- 
GitLab