├── .github ├── dependabot.yml └── workflows │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib ├── boolean_selector.dart └── src │ ├── all.dart │ ├── ast.dart │ ├── evaluator.dart │ ├── impl.dart │ ├── intersection_selector.dart │ ├── none.dart │ ├── parser.dart │ ├── scanner.dart │ ├── token.dart │ ├── union_selector.dart │ ├── validator.dart │ └── visitor.dart ├── pubspec.yaml └── test ├── equality_test.dart ├── evaluate_test.dart ├── parser_test.dart ├── scanner_test.dart ├── to_string_test.dart ├── validate_test.dart └── variables_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 26 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | test: 40 | needs: analyze 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | # Add macos-latest and/or windows-latest if relevant for this package. 46 | os: [ubuntu-latest] 47 | sdk: [3.1, dev] 48 | steps: 49 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 50 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 51 | with: 52 | sdk: ${{ matrix.sdk }} 53 | - id: install 54 | name: Install dependencies 55 | run: dart pub get 56 | - name: Run VM tests 57 | run: dart test --platform vm 58 | if: always() && steps.install.outcome == 'success' 59 | - name: Run Chrome tests 60 | run: dart test --platform chrome 61 | if: always() && steps.install.outcome == 'success' 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.2-wip 2 | 3 | * Increase the SDK minimum to `3.1.0`. 4 | 5 | ## 2.1.1 6 | 7 | * Increase the SDK minimum to `2.17.0`. 8 | * Populate the pubspec `repository` field. 9 | 10 | ## 2.1.0 11 | 12 | * Stable release for null safety. 13 | 14 | ## 2.0.0 15 | 16 | * Breaking: `BooleanSelector.evaluate` always takes a `bool Function(String)`. 17 | For use cases previously passing a `Set`, tear off the `.contains` 18 | method. For use cases passing an `Iterable` it may be worthwhile to 19 | first use `.toSet()` before tearing off `.contains`. 20 | 21 | ## 1.0.5 22 | 23 | * Update package metadata & add `example/` folder 24 | 25 | ## 1.0.4 26 | 27 | * Now requires Dart 2. 28 | 29 | ## 1.0.3 30 | 31 | * Work around an inference bug in the new common front-end. 32 | 33 | ## 1.0.2 34 | 35 | * Declare compatibility with `string_scanner` 1.0.0. 36 | 37 | ## 1.0.1 38 | 39 | * Fix all strong mode warnings. 40 | 41 | ## 1.0.0 42 | 43 | * Initial release. 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/boolean_selector 3 | 4 | [![Dart](https://github.com/dart-lang/boolean_selector/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/boolean_selector/actions/workflows/test-package.yml) 5 | [![Pub Package](https://img.shields.io/pub/v/boolean_selector.svg)](https://pub.dev/packages/boolean_selector) 6 | [![package publisher](https://img.shields.io/pub/publisher/boolean_selector.svg)](https://pub.dev/packages/boolean_selector/publisher) 7 | 8 | The `boolean_selector` package defines a simple and flexible syntax for boolean 9 | expressions. It can be used for filtering based on user-defined expressions. For 10 | example, the [`test`][test] package uses boolean selectors to allow users to 11 | define what platforms their tests support. 12 | 13 | [test]: https://github.com/dart-lang/test 14 | 15 | The boolean selector syntax is based on a simplified version of Dart's 16 | expression syntax. Selectors can contain identifiers, parentheses, and boolean 17 | operators, including `||`, `&&`, `!`, and `? :`. Any valid Dart identifier is 18 | allowed, and identifiers may also contain hyphens. For example, `chrome`, 19 | `chrome || content-shell`, and `js || (vm && linux)` are all valid boolean 20 | selectors. 21 | 22 | A boolean selector is parsed from a string using 23 | [`BooleanSelector.parse()`][parse], and evaluated against a set of variables 24 | using [`BooleanSelector.evaluate()`][evaluate]. The variables are supplied as 25 | a function that takes a variable name and returns its value. For example: 26 | 27 | [parse]: https://pub.dev/documentation/boolean_selector/latest/boolean_selector/BooleanSelector/BooleanSelector.parse.html 28 | 29 | [evaluate]: https://pub.dev/documentation/boolean_selector/latest/boolean_selector/BooleanSelector/evaluate.html 30 | 31 | ```dart 32 | import 'package:boolean_selector/boolean_selector.dart'; 33 | 34 | void main(List args) { 35 | var selector = BooleanSelector.parse("(x && y) || z"); 36 | print(selector.evaluate((variable) => args.contains(variable))); 37 | } 38 | ``` 39 | 40 | ## Versioning 41 | 42 | If this package adds new features to the boolean selector syntax, it will 43 | increment its major version number. This ensures that packages that expose the 44 | syntax to their users will be able to update their own minor versions, so their 45 | users can indicate that they rely on the new syntax. 46 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | - avoid_unused_constructor_parameters 6 | - cancel_subscriptions 7 | - package_api_docs 8 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:boolean_selector/boolean_selector.dart'; 2 | 3 | void main(List args) { 4 | var selector = BooleanSelector.parse('(x && y) || z'); 5 | print(selector.evaluate((variable) => args.contains(variable))); 6 | } 7 | -------------------------------------------------------------------------------- /lib/boolean_selector.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | import 'src/all.dart'; 8 | import 'src/impl.dart'; 9 | import 'src/none.dart'; 10 | 11 | /// A boolean expression that evaluates to `true` or `false` based on certain 12 | /// inputs. 13 | /// 14 | /// The syntax is mostly Dart's expression syntax restricted to boolean 15 | /// operations. See [the README][] for full details. 16 | /// 17 | /// [the README]: https://github.com/dart-lang/boolean_selector/blob/master/README.md 18 | /// 19 | /// Boolean selectors support structural equality. Two selectors that have the 20 | /// same parsed structure are considered equal. 21 | abstract class BooleanSelector { 22 | /// A selector that accepts all inputs. 23 | static const all = All(); 24 | 25 | /// A selector that accepts no inputs. 26 | static const none = None(); 27 | 28 | /// All the variables in this selector, in the order they appear. 29 | Iterable get variables; 30 | 31 | /// Parses [selector]. 32 | /// 33 | /// This will throw a [SourceSpanFormatException] if the selector is 34 | /// malformed or if it uses an undefined variable. 35 | factory BooleanSelector.parse(String selector) = BooleanSelectorImpl.parse; 36 | 37 | /// Returns whether the selector matches the given [semantics]. 38 | /// 39 | /// The [semantics] define which variables evaluate to `true` or `false`. When 40 | /// passed a variable name it should return the value of that variable. 41 | bool evaluate(bool Function(String variable) semantics); 42 | 43 | /// Returns a new [BooleanSelector] that matches only inputs matched by both 44 | /// `this` and [other]. 45 | BooleanSelector intersection(BooleanSelector other); 46 | 47 | /// Returns a new [BooleanSelector] that matches all inputs matched by either 48 | /// `this` or [other]. 49 | BooleanSelector union(BooleanSelector other); 50 | 51 | /// Throws a [FormatException] if any variables are undefined. 52 | /// 53 | /// The [isDefined] function should return `true` for any variables that are 54 | /// considered valid, and `false` for any invalid or undefined variables. 55 | void validate(bool Function(String variable) isDefined); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/all.dart: -------------------------------------------------------------------------------- 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 '../boolean_selector.dart'; 6 | 7 | /// A selector that matches all inputs. 8 | class All implements BooleanSelector { 9 | // TODO(nweiz): Stop explicitly providing a type argument when sdk#32412 is 10 | // fixed. 11 | @override 12 | final Iterable variables = const []; 13 | 14 | const All(); 15 | 16 | @override 17 | bool evaluate(bool Function(String variable) semantics) => true; 18 | 19 | @override 20 | BooleanSelector intersection(BooleanSelector other) => other; 21 | 22 | @override 23 | BooleanSelector union(BooleanSelector other) => this; 24 | 25 | @override 26 | void validate(bool Function(String variable) isDefined) {} 27 | 28 | @override 29 | String toString() => ''; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/ast.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | import 'visitor.dart'; 8 | 9 | /// The superclass of nodes in the boolean selector abstract syntax tree. 10 | abstract class Node { 11 | /// The span indicating where this node came from. 12 | /// 13 | /// This is a [FileSpan] because the nodes are parsed from a single continuous 14 | /// string, but the string itself isn't actually a file. It might come from a 15 | /// statically-parsed annotation or from a parameter. 16 | /// 17 | /// This may be `null` for nodes without source information. 18 | FileSpan? get span; 19 | 20 | /// All the variables in this node, in the order they appear. 21 | Iterable get variables; 22 | 23 | /// Calls the appropriate [Visitor] method on `this` and returns the result. 24 | T accept(Visitor visitor); 25 | } 26 | 27 | /// A single variable. 28 | class VariableNode implements Node { 29 | @override 30 | final FileSpan? span; 31 | 32 | /// The variable name. 33 | final String name; 34 | 35 | @override 36 | Iterable get variables => [name]; 37 | 38 | VariableNode(this.name, [this.span]); 39 | 40 | @override 41 | T accept(Visitor visitor) => visitor.visitVariable(this); 42 | 43 | @override 44 | String toString() => name; 45 | 46 | @override 47 | bool operator ==(Object other) => other is VariableNode && name == other.name; 48 | 49 | @override 50 | int get hashCode => name.hashCode; 51 | } 52 | 53 | /// A negation expression. 54 | class NotNode implements Node { 55 | @override 56 | final FileSpan? span; 57 | 58 | /// The expression being negated. 59 | final Node child; 60 | 61 | @override 62 | Iterable get variables => child.variables; 63 | 64 | NotNode(this.child, [this.span]); 65 | 66 | @override 67 | T accept(Visitor visitor) => visitor.visitNot(this); 68 | 69 | @override 70 | String toString() => 71 | child is VariableNode || child is NotNode ? '!$child' : '!($child)'; 72 | 73 | @override 74 | bool operator ==(Object other) => other is NotNode && child == other.child; 75 | 76 | @override 77 | int get hashCode => ~child.hashCode; 78 | } 79 | 80 | /// An or expression. 81 | class OrNode implements Node { 82 | @override 83 | FileSpan? get span => _expandSafe(left.span, right.span); 84 | 85 | /// The left-hand branch of the expression. 86 | final Node left; 87 | 88 | /// The right-hand branch of the expression. 89 | final Node right; 90 | 91 | @override 92 | Iterable get variables sync* { 93 | yield* left.variables; 94 | yield* right.variables; 95 | } 96 | 97 | OrNode(this.left, this.right); 98 | 99 | @override 100 | T accept(Visitor visitor) => visitor.visitOr(this); 101 | 102 | @override 103 | String toString() { 104 | var string1 = left is AndNode || left is ConditionalNode ? '($left)' : left; 105 | var string2 = 106 | right is AndNode || right is ConditionalNode ? '($right)' : right; 107 | 108 | return '$string1 || $string2'; 109 | } 110 | 111 | @override 112 | bool operator ==(Object other) => 113 | other is OrNode && left == other.left && right == other.right; 114 | 115 | @override 116 | int get hashCode => left.hashCode ^ right.hashCode; 117 | } 118 | 119 | /// An and expression. 120 | class AndNode implements Node { 121 | @override 122 | FileSpan? get span => _expandSafe(left.span, right.span); 123 | 124 | /// The left-hand branch of the expression. 125 | final Node left; 126 | 127 | /// The right-hand branch of the expression. 128 | final Node right; 129 | 130 | @override 131 | Iterable get variables sync* { 132 | yield* left.variables; 133 | yield* right.variables; 134 | } 135 | 136 | AndNode(this.left, this.right); 137 | 138 | @override 139 | T accept(Visitor visitor) => visitor.visitAnd(this); 140 | 141 | @override 142 | String toString() { 143 | var string1 = left is OrNode || left is ConditionalNode ? '($left)' : left; 144 | var string2 = 145 | right is OrNode || right is ConditionalNode ? '($right)' : right; 146 | 147 | return '$string1 && $string2'; 148 | } 149 | 150 | @override 151 | bool operator ==(Object other) => 152 | other is AndNode && left == other.left && right == other.right; 153 | 154 | @override 155 | int get hashCode => left.hashCode ^ right.hashCode; 156 | } 157 | 158 | /// A ternary conditional expression. 159 | class ConditionalNode implements Node { 160 | @override 161 | FileSpan? get span => _expandSafe(condition.span, whenFalse.span); 162 | 163 | /// The condition expression to check. 164 | final Node condition; 165 | 166 | /// The branch to run if the condition is true. 167 | final Node whenTrue; 168 | 169 | /// The branch to run if the condition is false. 170 | final Node whenFalse; 171 | 172 | @override 173 | Iterable get variables sync* { 174 | yield* condition.variables; 175 | yield* whenTrue.variables; 176 | yield* whenFalse.variables; 177 | } 178 | 179 | ConditionalNode(this.condition, this.whenTrue, this.whenFalse); 180 | 181 | @override 182 | T accept(Visitor visitor) => visitor.visitConditional(this); 183 | 184 | @override 185 | String toString() { 186 | var conditionString = 187 | condition is ConditionalNode ? '($condition)' : condition; 188 | var trueString = whenTrue is ConditionalNode ? '($whenTrue)' : whenTrue; 189 | return '$conditionString ? $trueString : $whenFalse'; 190 | } 191 | 192 | @override 193 | bool operator ==(Object other) => 194 | other is ConditionalNode && 195 | condition == other.condition && 196 | whenTrue == other.whenTrue && 197 | whenFalse == other.whenFalse; 198 | 199 | @override 200 | int get hashCode => 201 | condition.hashCode ^ whenTrue.hashCode ^ whenFalse.hashCode; 202 | } 203 | 204 | /// Like [FileSpan.expand], except if [start] and [end] are `null` or from 205 | /// different files it returns `null` rather than throwing an error. 206 | FileSpan? _expandSafe(FileSpan? start, FileSpan? end) { 207 | if (start == null || end == null) return null; 208 | if (start.file != end.file) return null; 209 | return start.expand(end); 210 | } 211 | -------------------------------------------------------------------------------- /lib/src/evaluator.dart: -------------------------------------------------------------------------------- 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 'ast.dart'; 6 | import 'visitor.dart'; 7 | 8 | /// A visitor for evaluating boolean selectors against a specific set of 9 | /// semantics. 10 | class Evaluator implements Visitor { 11 | final bool Function(String variable) _semantics; 12 | 13 | Evaluator(this._semantics); 14 | 15 | @override 16 | bool visitVariable(VariableNode node) => _semantics(node.name); 17 | 18 | @override 19 | bool visitNot(NotNode node) => !node.child.accept(this); 20 | 21 | @override 22 | bool visitOr(OrNode node) => 23 | node.left.accept(this) || node.right.accept(this); 24 | 25 | @override 26 | bool visitAnd(AndNode node) => 27 | node.left.accept(this) && node.right.accept(this); 28 | 29 | @override 30 | bool visitConditional(ConditionalNode node) => node.condition.accept(this) 31 | ? node.whenTrue.accept(this) 32 | : node.whenFalse.accept(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/impl.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | import '../boolean_selector.dart'; 8 | import 'ast.dart'; 9 | import 'evaluator.dart'; 10 | import 'intersection_selector.dart'; 11 | import 'parser.dart'; 12 | import 'union_selector.dart'; 13 | import 'validator.dart'; 14 | 15 | /// The concrete implementation of a [BooleanSelector] parsed from a string. 16 | /// 17 | /// This is separate from [BooleanSelector] so that [intersection] and [union] 18 | /// can check to see whether they're passed a [BooleanSelectorImpl] or a 19 | /// different class that implements [BooleanSelector]. 20 | class BooleanSelectorImpl implements BooleanSelector { 21 | /// The parsed AST. 22 | final Node _selector; 23 | 24 | /// Parses [selector]. 25 | /// 26 | /// This will throw a [SourceSpanFormatException] if the selector is 27 | /// malformed or if it uses an undefined variable. 28 | BooleanSelectorImpl.parse(String selector) 29 | : _selector = Parser(selector).parse(); 30 | 31 | BooleanSelectorImpl._(this._selector); 32 | 33 | @override 34 | Iterable get variables => _selector.variables; 35 | 36 | @override 37 | bool evaluate(bool Function(String variable) semantics) => 38 | _selector.accept(Evaluator(semantics)); 39 | 40 | @override 41 | BooleanSelector intersection(BooleanSelector other) { 42 | if (other == BooleanSelector.all) return this; 43 | if (other == BooleanSelector.none) return other; 44 | return other is BooleanSelectorImpl 45 | ? BooleanSelectorImpl._(AndNode(_selector, other._selector)) 46 | : IntersectionSelector(this, other); 47 | } 48 | 49 | @override 50 | BooleanSelector union(BooleanSelector other) { 51 | if (other == BooleanSelector.all) return other; 52 | if (other == BooleanSelector.none) return this; 53 | return other is BooleanSelectorImpl 54 | ? BooleanSelectorImpl._(OrNode(_selector, other._selector)) 55 | : UnionSelector(this, other); 56 | } 57 | 58 | @override 59 | void validate(bool Function(String variable) isDefined) { 60 | _selector.accept(Validator(isDefined)); 61 | } 62 | 63 | @override 64 | String toString() => _selector.toString(); 65 | 66 | @override 67 | bool operator ==(Object other) => 68 | other is BooleanSelectorImpl && _selector == other._selector; 69 | 70 | @override 71 | int get hashCode => _selector.hashCode; 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/intersection_selector.dart: -------------------------------------------------------------------------------- 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 '../boolean_selector.dart'; 6 | import 'union_selector.dart'; 7 | 8 | /// A selector that matches inputs that both of its sub-selectors match. 9 | class IntersectionSelector implements BooleanSelector { 10 | final BooleanSelector _selector1; 11 | final BooleanSelector _selector2; 12 | 13 | @override 14 | Iterable get variables sync* { 15 | yield* _selector1.variables; 16 | yield* _selector2.variables; 17 | } 18 | 19 | IntersectionSelector(this._selector1, this._selector2); 20 | 21 | @override 22 | bool evaluate(bool Function(String variable) semantics) => 23 | _selector1.evaluate(semantics) && _selector2.evaluate(semantics); 24 | 25 | @override 26 | BooleanSelector intersection(BooleanSelector other) => 27 | IntersectionSelector(this, other); 28 | 29 | @override 30 | BooleanSelector union(BooleanSelector other) => UnionSelector(this, other); 31 | 32 | @override 33 | void validate(bool Function(String variable) isDefined) { 34 | _selector1.validate(isDefined); 35 | _selector2.validate(isDefined); 36 | } 37 | 38 | @override 39 | String toString() => '($_selector1) && ($_selector2)'; 40 | 41 | @override 42 | bool operator ==(Object other) => 43 | other is IntersectionSelector && 44 | _selector1 == other._selector1 && 45 | _selector2 == other._selector2; 46 | 47 | @override 48 | int get hashCode => _selector1.hashCode ^ _selector2.hashCode; 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/none.dart: -------------------------------------------------------------------------------- 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 '../boolean_selector.dart'; 6 | 7 | /// A selector that matches no inputs. 8 | class None implements BooleanSelector { 9 | @override 10 | final Iterable variables = const []; 11 | 12 | const None(); 13 | 14 | @override 15 | bool evaluate(bool Function(String variable) semantics) => false; 16 | 17 | @override 18 | BooleanSelector intersection(BooleanSelector other) => this; 19 | 20 | @override 21 | BooleanSelector union(BooleanSelector other) => other; 22 | 23 | @override 24 | void validate(bool Function(String) isDefined) {} 25 | 26 | @override 27 | String toString() => ''; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/parser.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | import 'ast.dart'; 8 | import 'scanner.dart'; 9 | import 'token.dart'; 10 | 11 | /// A class for parsing a boolean selector. 12 | /// 13 | /// Boolean selectors use a stripped-down version of the Dart expression syntax 14 | /// that only contains variables, parentheses, and boolean operators. Variables 15 | /// may also contain dashes, contrary to Dart's syntax; this allows consistency 16 | /// with command-line arguments. 17 | class Parser { 18 | /// The scanner that tokenizes the selector. 19 | final Scanner _scanner; 20 | 21 | Parser(String selector) : _scanner = Scanner(selector); 22 | 23 | /// Parses the selector. 24 | /// 25 | /// This must only be called once per parser. 26 | Node parse() { 27 | var selector = _conditional(); 28 | 29 | if (_scanner.peek().type != TokenType.endOfFile) { 30 | throw SourceSpanFormatException( 31 | 'Expected end of input.', _scanner.peek().span); 32 | } 33 | 34 | return selector; 35 | } 36 | 37 | /// Parses a conditional: 38 | /// 39 | /// conditionalExpression: 40 | /// logicalOrExpression ("?" conditionalExpression ":" 41 | /// conditionalExpression)? 42 | Node _conditional() { 43 | var condition = _or(); 44 | if (!_scanner.scan(TokenType.questionMark)) return condition; 45 | 46 | var whenTrue = _conditional(); 47 | if (!_scanner.scan(TokenType.colon)) { 48 | throw SourceSpanFormatException('Expected ":".', _scanner.peek().span); 49 | } 50 | 51 | var whenFalse = _conditional(); 52 | return ConditionalNode(condition, whenTrue, whenFalse); 53 | } 54 | 55 | /// Parses a logical or: 56 | /// 57 | /// logicalOrExpression: 58 | /// logicalAndExpression ("||" logicalOrExpression)? 59 | Node _or() { 60 | var left = _and(); 61 | if (!_scanner.scan(TokenType.or)) return left; 62 | return OrNode(left, _or()); 63 | } 64 | 65 | /// Parses a logical and: 66 | /// 67 | /// logicalAndExpression: 68 | /// simpleExpression ("&&" logicalAndExpression)? 69 | Node _and() { 70 | var left = _simpleExpression(); 71 | if (!_scanner.scan(TokenType.and)) return left; 72 | return AndNode(left, _and()); 73 | } 74 | 75 | /// Parses a simple expression: 76 | /// 77 | /// simpleExpression: 78 | /// "!" simpleExpression | 79 | /// "(" conditionalExpression ")" | 80 | /// IDENTIFIER 81 | Node _simpleExpression() { 82 | var token = _scanner.next(); 83 | switch (token.type) { 84 | case TokenType.not: 85 | var child = _simpleExpression(); 86 | return NotNode(child, token.span.expand(child.span!)); 87 | 88 | case TokenType.leftParen: 89 | var child = _conditional(); 90 | if (!_scanner.scan(TokenType.rightParen)) { 91 | throw SourceSpanFormatException( 92 | 'Expected ")".', _scanner.peek().span); 93 | } 94 | return child; 95 | 96 | case TokenType.identifier: 97 | return VariableNode((token as IdentifierToken).name, token.span); 98 | 99 | default: 100 | throw SourceSpanFormatException('Expected expression.', token.span); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/scanner.dart: -------------------------------------------------------------------------------- 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 | 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 | 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 | 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 | 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 | 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 | Token next() { 50 | var token = _next ?? _readNext(); 51 | _endOfFileEmitted = token.type == TokenType.endOfFile; 52 | _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 | bool scan(TokenType type) { 62 | if (peek().type != type) return false; 63 | next(); 64 | return true; 65 | } 66 | 67 | /// Scan and return the next token in the stream. 68 | Token _readNext() { 69 | if (_endOfFileEmitted) throw StateError('No more tokens.'); 70 | 71 | _consumeWhitespace(); 72 | if (_scanner.isDone) { 73 | return Token(TokenType.endOfFile, _scanner.spanFrom(_scanner.state)); 74 | } 75 | 76 | return switch (_scanner.peekChar()) { 77 | 0x28 /* ( */ => _scanOperator(TokenType.leftParen), 78 | 0x29 /* ) */ => _scanOperator(TokenType.rightParen), 79 | 0x3F /* ? */ => _scanOperator(TokenType.questionMark), 80 | 0x3A /* : */ => _scanOperator(TokenType.colon), 81 | 0x21 /* ! */ => _scanOperator(TokenType.not), 82 | 0x7C /* | */ => _scanOr(), 83 | 0x26 /* & */ => _scanAnd(), 84 | _ => _scanIdentifier() 85 | }; 86 | } 87 | 88 | /// Scans a single-character operator and returns a token of type [type]. 89 | /// 90 | /// This assumes that the caller has already verified that the next character 91 | /// is correct for the given operator. 92 | Token _scanOperator(TokenType type) { 93 | var start = _scanner.state; 94 | _scanner.readChar(); 95 | return Token(type, _scanner.spanFrom(start)); 96 | } 97 | 98 | /// Scans a `||` operator and returns the appropriate token. 99 | /// 100 | /// This validates that the next two characters are `||`. 101 | Token _scanOr() { 102 | var start = _scanner.state; 103 | _scanner.expect('||'); 104 | return Token(TokenType.or, _scanner.spanFrom(start)); 105 | } 106 | 107 | /// Scans a `&&` operator and returns the appropriate token. 108 | /// 109 | /// This validates that the next two characters are `&&`. 110 | Token _scanAnd() { 111 | var start = _scanner.state; 112 | _scanner.expect('&&'); 113 | return Token(TokenType.and, _scanner.spanFrom(start)); 114 | } 115 | 116 | /// Scans and returns an identifier token. 117 | Token _scanIdentifier() { 118 | _scanner.expect(_hyphenatedIdentifier, name: 'expression'); 119 | return IdentifierToken(_scanner.lastMatch![0]!, _scanner.lastSpan!); 120 | } 121 | 122 | /// Consumes all whitespace and comments immediately following the cursor's 123 | /// current position. 124 | void _consumeWhitespace() { 125 | while (_scanner.scan(_whitespaceAndSingleLineComments) || 126 | _multiLineComment()) { 127 | // Do nothing. 128 | } 129 | } 130 | 131 | /// Consumes a single multi-line comment. 132 | /// 133 | /// Returns whether or not a comment was consumed. 134 | bool _multiLineComment() { 135 | if (!_scanner.scan('/*')) return false; 136 | 137 | while (_scanner.scan(_multiLineCommentBody) || _multiLineComment()) { 138 | // Do nothing. 139 | } 140 | _scanner.expect('*/'); 141 | 142 | return true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/token.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | /// A token in a boolean selector. 8 | class Token { 9 | /// The type of the token. 10 | final TokenType type; 11 | 12 | /// The span indicating where this token came from. 13 | /// 14 | /// This is a [FileSpan] because the tokens are parsed from a single 15 | /// continuous string, but the string itself isn't actually a file. It might 16 | /// come from a statically-parsed annotation or from a parameter. 17 | final FileSpan span; 18 | 19 | Token(this.type, this.span); 20 | } 21 | 22 | /// A token representing an identifier. 23 | class IdentifierToken implements Token { 24 | @override 25 | final type = TokenType.identifier; 26 | @override 27 | final FileSpan span; 28 | 29 | /// The name of the identifier. 30 | final String name; 31 | 32 | IdentifierToken(this.name, this.span); 33 | 34 | @override 35 | String toString() => 'identifier "$name"'; 36 | } 37 | 38 | /// An enumeration of types of tokens. 39 | class TokenType { 40 | /// A `(` character. 41 | static const leftParen = TokenType._('left paren'); 42 | 43 | /// A `)` character. 44 | static const rightParen = TokenType._('right paren'); 45 | 46 | /// A `||` sequence. 47 | static const or = TokenType._('or'); 48 | 49 | /// A `&&` sequence. 50 | static const and = TokenType._('and'); 51 | 52 | /// A `!` character. 53 | static const not = TokenType._('not'); 54 | 55 | /// A `?` character. 56 | static const questionMark = TokenType._('question mark'); 57 | 58 | /// A `:` character. 59 | static const colon = TokenType._('colon'); 60 | 61 | /// A named identifier. 62 | static const identifier = TokenType._('identifier'); 63 | 64 | /// The end of the selector. 65 | static const endOfFile = TokenType._('end of file'); 66 | 67 | /// The name of the token type. 68 | final String name; 69 | 70 | const TokenType._(this.name); 71 | 72 | @override 73 | String toString() => name; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/union_selector.dart: -------------------------------------------------------------------------------- 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 '../boolean_selector.dart'; 6 | import 'intersection_selector.dart'; 7 | 8 | /// A selector that matches inputs that either of its sub-selectors match. 9 | class UnionSelector implements BooleanSelector { 10 | final BooleanSelector _selector1; 11 | final BooleanSelector _selector2; 12 | 13 | UnionSelector(this._selector1, this._selector2); 14 | 15 | @override 16 | List get variables => 17 | _selector1.variables.toList()..addAll(_selector2.variables); 18 | 19 | @override 20 | bool evaluate(bool Function(String variable) semantics) => 21 | _selector1.evaluate(semantics) || _selector2.evaluate(semantics); 22 | 23 | @override 24 | BooleanSelector intersection(BooleanSelector other) => 25 | IntersectionSelector(this, other); 26 | 27 | @override 28 | BooleanSelector union(BooleanSelector other) => UnionSelector(this, other); 29 | 30 | @override 31 | void validate(bool Function(String variable) isDefined) { 32 | _selector1.validate(isDefined); 33 | _selector2.validate(isDefined); 34 | } 35 | 36 | @override 37 | String toString() => '($_selector1) && ($_selector2)'; 38 | 39 | @override 40 | bool operator ==(Object other) => 41 | other is UnionSelector && 42 | _selector1 == other._selector1 && 43 | _selector2 == other._selector2; 44 | 45 | @override 46 | int get hashCode => _selector1.hashCode ^ _selector2.hashCode; 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/validator.dart: -------------------------------------------------------------------------------- 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:source_span/source_span.dart'; 6 | 7 | import 'ast.dart'; 8 | import 'visitor.dart'; 9 | 10 | typedef _IsDefined = bool Function(String variable); 11 | 12 | /// An AST visitor that ensures that all variables are valid. 13 | class Validator extends RecursiveVisitor { 14 | final _IsDefined _isDefined; 15 | 16 | Validator(this._isDefined); 17 | 18 | @override 19 | void visitVariable(VariableNode node) { 20 | if (_isDefined(node.name)) return; 21 | throw SourceSpanFormatException('Undefined variable.', node.span); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/visitor.dart: -------------------------------------------------------------------------------- 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 'ast.dart'; 6 | 7 | /// The interface for visitors of the boolean selector AST. 8 | abstract class Visitor { 9 | T visitVariable(VariableNode node); 10 | T visitNot(NotNode node); 11 | T visitOr(OrNode node); 12 | T visitAnd(AndNode node); 13 | T visitConditional(ConditionalNode node); 14 | } 15 | 16 | /// An abstract superclass for side-effect-based visitors. 17 | /// 18 | /// The default implementations of this visitor's methods just traverse the AST 19 | /// and do nothing with it. 20 | abstract class RecursiveVisitor implements Visitor { 21 | const RecursiveVisitor(); 22 | 23 | @override 24 | void visitVariable(VariableNode node) {} 25 | 26 | @override 27 | void visitNot(NotNode node) { 28 | node.child.accept(this); 29 | } 30 | 31 | @override 32 | void visitOr(OrNode node) { 33 | node.left.accept(this); 34 | node.right.accept(this); 35 | } 36 | 37 | @override 38 | void visitAnd(AndNode node) { 39 | node.left.accept(this); 40 | node.right.accept(this); 41 | } 42 | 43 | @override 44 | void visitConditional(ConditionalNode node) { 45 | node.condition.accept(this); 46 | node.whenTrue.accept(this); 47 | node.whenFalse.accept(this); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: boolean_selector 2 | version: 2.1.2-wip 3 | description: >- 4 | A flexible syntax for boolean expressions, based on a simplified version of 5 | Dart's expression syntax. 6 | repository: https://github.com/dart-lang/boolean_selector 7 | 8 | environment: 9 | sdk: ^3.1.0 10 | 11 | dependencies: 12 | source_span: ^1.8.0 13 | string_scanner: ^1.1.0 14 | 15 | dev_dependencies: 16 | dart_flutter_team_lints: ^3.0.0 17 | test: ^1.16.0 18 | -------------------------------------------------------------------------------- /test/equality_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/boolean_selector.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('variable', () { 10 | _expectEqualsSelf('foo'); 11 | }); 12 | 13 | test('not', () { 14 | _expectEqualsSelf('!foo'); 15 | }); 16 | 17 | test('or', () { 18 | _expectEqualsSelf('foo || bar'); 19 | }); 20 | 21 | test('and', () { 22 | _expectEqualsSelf('foo && bar'); 23 | }); 24 | 25 | test('conditional', () { 26 | _expectEqualsSelf('foo ? bar : baz'); 27 | }); 28 | 29 | test('all', () { 30 | expect(BooleanSelector.all, equals(BooleanSelector.all)); 31 | }); 32 | 33 | test('none', () { 34 | expect(BooleanSelector.none, equals(BooleanSelector.none)); 35 | }); 36 | 37 | test("redundant parens don't matter", () { 38 | expect(BooleanSelector.parse('foo && (bar && baz)'), 39 | equals(BooleanSelector.parse('foo && (bar && baz)'))); 40 | }); 41 | 42 | test('meaningful parens do matter', () { 43 | expect(BooleanSelector.parse('(foo && bar) || baz'), 44 | equals(BooleanSelector.parse('foo && bar || baz'))); 45 | }); 46 | } 47 | 48 | void _expectEqualsSelf(String selector) { 49 | expect( 50 | BooleanSelector.parse(selector), equals(BooleanSelector.parse(selector))); 51 | } 52 | -------------------------------------------------------------------------------- /test/evaluate_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/boolean_selector.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('operator:', () { 10 | test('conditional', () { 11 | _expectEval('true ? true : false', true); 12 | _expectEval('true ? false : true', false); 13 | _expectEval('false ? true : false', false); 14 | _expectEval('false ? false : true', true); 15 | }); 16 | 17 | test('or', () { 18 | _expectEval('true || true', true); 19 | _expectEval('true || false', true); 20 | _expectEval('false || true', true); 21 | _expectEval('false || false', false); 22 | }); 23 | 24 | test('and', () { 25 | _expectEval('true && true', true); 26 | _expectEval('true && false', false); 27 | _expectEval('false && true', false); 28 | _expectEval('false && false', false); 29 | }); 30 | 31 | test('not', () { 32 | _expectEval('!true', false); 33 | _expectEval('!false', true); 34 | }); 35 | }); 36 | 37 | test('with a semantics function', () { 38 | _expectEval('foo', false, semantics: (variable) => variable.contains('a')); 39 | _expectEval('bar', true, semantics: (variable) => variable.contains('a')); 40 | _expectEval('baz', true, semantics: (variable) => variable.contains('a')); 41 | }); 42 | } 43 | 44 | /// Asserts that [expression] evaluates to [result] against [semantics]. 45 | /// 46 | /// By default, "true" is true and all other variables are "false". 47 | void _expectEval(String expression, bool result, 48 | {bool Function(String variable)? semantics}) { 49 | expect(_eval(expression, semantics: semantics), equals(result), 50 | reason: 'Expected "$expression" to evaluate to $result.'); 51 | } 52 | 53 | /// Returns the result of evaluating [expression] on [semantics]. 54 | /// 55 | /// By default, "true" is true and all other variables are "false". 56 | bool _eval(String expression, {bool Function(String variable)? semantics}) { 57 | var selector = BooleanSelector.parse(expression); 58 | return selector.evaluate(semantics ?? (v) => v == 'true'); 59 | } 60 | -------------------------------------------------------------------------------- /test/parser_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/src/ast.dart'; 6 | import 'package:boolean_selector/src/parser.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | /// A matcher that asserts that a value is a [ConditionalNode]. 10 | const _isConditionalNode = TypeMatcher(); 11 | 12 | /// A matcher that asserts that a value is an [OrNode]. 13 | const _isOrNode = TypeMatcher(); 14 | 15 | /// A matcher that asserts that a value is an [AndNode]. 16 | const _isAndNode = TypeMatcher(); 17 | 18 | /// A matcher that asserts that a value is a [NotNode]. 19 | const _isNotNode = TypeMatcher(); 20 | 21 | void main() { 22 | group('parses a conditional expression', () { 23 | test('with identifiers', () { 24 | var node = _parse(' a ? b : c '); 25 | expect(node.toString(), equals('a ? b : c')); 26 | 27 | expect(node.span, isNotNull); 28 | expect(node.span!.text, equals('a ? b : c')); 29 | expect(node.span!.start.offset, equals(2)); 30 | expect(node.span!.end.offset, equals(11)); 31 | }); 32 | 33 | test('with nested ors', () { 34 | // Should parse as "(a || b) ? (c || d) : (e || f)". 35 | // Should not parse as "a || (b ? (c || d) : (e || f))". 36 | // Should not parse as "((a || b) ? (c || d) : e) || f". 37 | // Should not parse as "a || (b ? (c || d) : e) || f". 38 | _expectToString('a || b ? c || d : e || f', 'a || b ? c || d : e || f'); 39 | }); 40 | 41 | test('with a conditional expression as branch 1', () { 42 | // Should parse as "a ? (b ? c : d) : e". 43 | var node = _parse('a ? b ? c : d : e'); 44 | expect(node, _isConditionalNode); 45 | node as ConditionalNode; // promote node 46 | 47 | expect(node.condition, _isVar('a')); 48 | expect(node.whenFalse, _isVar('e')); 49 | 50 | expect(node.whenTrue, _isConditionalNode); 51 | var whenTrue = node.whenTrue as ConditionalNode; 52 | expect(whenTrue.condition, _isVar('b')); 53 | expect(whenTrue.whenTrue, _isVar('c')); 54 | expect(whenTrue.whenFalse, _isVar('d')); 55 | }); 56 | 57 | test('with a conditional expression as branch 2', () { 58 | // Should parse as "a ? b : (c ? d : e)". 59 | // Should not parse as "(a ? b : c) ? d : e". 60 | var node = _parse('a ? b : c ? d : e'); 61 | expect(node, _isConditionalNode); 62 | node as ConditionalNode; //promote node 63 | 64 | expect(node.condition, _isVar('a')); 65 | expect(node.whenTrue, _isVar('b')); 66 | 67 | expect(node.whenFalse, _isConditionalNode); 68 | var whenFalse = node.whenFalse as ConditionalNode; 69 | expect(whenFalse.condition, _isVar('c')); 70 | expect(whenFalse.whenTrue, _isVar('d')); 71 | expect(whenFalse.whenFalse, _isVar('e')); 72 | }); 73 | 74 | group('which must have', () { 75 | test('an expression after the ?', () { 76 | expect(() => _parse('a ?'), throwsFormatException); 77 | expect(() => _parse('a ? && b'), throwsFormatException); 78 | }); 79 | 80 | test('a :', () { 81 | expect(() => _parse('a ? b'), throwsFormatException); 82 | expect(() => _parse('a ? b && c'), throwsFormatException); 83 | }); 84 | 85 | test('an expression after the :', () { 86 | expect(() => _parse('a ? b :'), throwsFormatException); 87 | expect(() => _parse('a ? b : && c'), throwsFormatException); 88 | }); 89 | }); 90 | }); 91 | 92 | group('parses an or expression', () { 93 | test('with identifiers', () { 94 | var node = _parse(' a || b '); 95 | expect(node, _isOrNode); 96 | node as OrNode; //promote node 97 | 98 | expect(node.left, _isVar('a')); 99 | expect(node.right, _isVar('b')); 100 | 101 | expect(node.span, isNotNull); 102 | expect(node.span!.text, equals('a || b')); 103 | expect(node.span!.start.offset, equals(2)); 104 | expect(node.span!.end.offset, equals(8)); 105 | }); 106 | 107 | test('with nested ands', () { 108 | // Should parse as "(a && b) || (c && d)". 109 | // Should not parse as "a && (b || c) && d". 110 | var node = _parse('a && b || c && d'); 111 | expect(node, _isOrNode); 112 | node as OrNode; //promote node 113 | 114 | expect(node.left, _isAndNode); 115 | var left = node.left as AndNode; 116 | expect(left.left, _isVar('a')); 117 | expect(left.right, _isVar('b')); 118 | 119 | expect(node.right, _isAndNode); 120 | var right = node.right as AndNode; 121 | expect(right.left, _isVar('c')); 122 | expect(right.right, _isVar('d')); 123 | }); 124 | 125 | test('with trailing ors', () { 126 | // Should parse as "a || (b || (c || d))", although it doesn't affect the 127 | // semantics. 128 | var node = _parse('a || b || c || d'); 129 | 130 | for (var variable in ['a', 'b', 'c']) { 131 | expect(node, _isOrNode); 132 | node as OrNode; //promote node 133 | 134 | expect(node.left, _isVar(variable)); 135 | node = node.right; 136 | } 137 | expect(node, _isVar('d')); 138 | }); 139 | 140 | test('which must have an expression after the ||', () { 141 | expect(() => _parse('a ||'), throwsFormatException); 142 | expect(() => _parse('a || && b'), throwsFormatException); 143 | }); 144 | }); 145 | 146 | group('parses an and expression', () { 147 | test('with identifiers', () { 148 | var node = _parse(' a && b '); 149 | expect(node, _isAndNode); 150 | node as AndNode; //promote node 151 | 152 | expect(node.left, _isVar('a')); 153 | expect(node.right, _isVar('b')); 154 | 155 | expect(node.span, isNotNull); 156 | expect(node.span!.text, equals('a && b')); 157 | expect(node.span!.start.offset, equals(2)); 158 | expect(node.span!.end.offset, equals(8)); 159 | }); 160 | 161 | test('with nested nots', () { 162 | // Should parse as "(!a) && (!b)", obviously. 163 | // Should not parse as "!(a && (!b))". 164 | var node = _parse('!a && !b'); 165 | expect(node, _isAndNode); 166 | node as AndNode; //promote node 167 | 168 | expect(node.left, _isNotNode); 169 | var left = node.left as NotNode; 170 | expect(left.child, _isVar('a')); 171 | 172 | expect(node.right, _isNotNode); 173 | var right = node.right as NotNode; 174 | expect(right.child, _isVar('b')); 175 | }); 176 | 177 | test('with trailing ands', () { 178 | // Should parse as "a && (b && (c && d))", although it doesn't affect the 179 | // semantics since . 180 | var node = _parse('a && b && c && d'); 181 | 182 | for (var variable in ['a', 'b', 'c']) { 183 | expect(node, _isAndNode); 184 | node as AndNode; //promote node 185 | 186 | expect(node.left, _isVar(variable)); 187 | node = node.right; 188 | } 189 | expect(node, _isVar('d')); 190 | }); 191 | 192 | test('which must have an expression after the &&', () { 193 | expect(() => _parse('a &&'), throwsFormatException); 194 | expect(() => _parse('a && && b'), throwsFormatException); 195 | }); 196 | }); 197 | 198 | group('parses a not expression', () { 199 | test('with an identifier', () { 200 | var node = _parse(' ! a '); 201 | expect(node, _isNotNode); 202 | node as NotNode; //promote node 203 | expect(node.child, _isVar('a')); 204 | 205 | expect(node.span, isNotNull); 206 | expect(node.span!.text, equals('! a')); 207 | expect(node.span!.start.offset, equals(2)); 208 | expect(node.span!.end.offset, equals(5)); 209 | }); 210 | 211 | test('with a parenthesized expression', () { 212 | var node = _parse('!(a || b)'); 213 | expect(node, _isNotNode); 214 | node as NotNode; //promote node 215 | 216 | expect(node.child, _isOrNode); 217 | var child = node.child as OrNode; 218 | expect(child.left, _isVar('a')); 219 | expect(child.right, _isVar('b')); 220 | }); 221 | 222 | test('with a nested not', () { 223 | var node = _parse('!!a'); 224 | expect(node, _isNotNode); 225 | node as NotNode; //promote node 226 | 227 | expect(node.child, _isNotNode); 228 | var child = node.child as NotNode; 229 | expect(child.child, _isVar('a')); 230 | }); 231 | 232 | test('which must have an expression after the !', () { 233 | expect(() => _parse('!'), throwsFormatException); 234 | expect(() => _parse('! && a'), throwsFormatException); 235 | }); 236 | }); 237 | 238 | group('parses a parenthesized expression', () { 239 | test('with an identifier', () { 240 | var node = _parse('(a)'); 241 | expect(node, _isVar('a')); 242 | }); 243 | 244 | test('controls precedence', () { 245 | // Without parentheses, this would parse as "(a || b) ? c : d". 246 | var node = _parse('a || (b ? c : d)'); 247 | 248 | expect(node, _isOrNode); 249 | node as OrNode; //promote node 250 | 251 | expect(node.left, _isVar('a')); 252 | 253 | expect(node.right, _isConditionalNode); 254 | var right = node.right as ConditionalNode; 255 | expect(right.condition, _isVar('b')); 256 | expect(right.whenTrue, _isVar('c')); 257 | expect(right.whenFalse, _isVar('d')); 258 | }); 259 | 260 | group('which must have', () { 261 | test('an expression within the ()', () { 262 | expect(() => _parse('()'), throwsFormatException); 263 | expect(() => _parse('( && a )'), throwsFormatException); 264 | }); 265 | 266 | test('a matching )', () { 267 | expect(() => _parse('( a'), throwsFormatException); 268 | }); 269 | }); 270 | }); 271 | 272 | group('disallows', () { 273 | test('an empty selector', () { 274 | expect(() => _parse(''), throwsFormatException); 275 | }); 276 | 277 | test('too many expressions', () { 278 | expect(() => _parse('a b'), throwsFormatException); 279 | }); 280 | }); 281 | } 282 | 283 | /// Parses [selector] and returns its root node. 284 | Node _parse(String selector) => Parser(selector).parse(); 285 | 286 | /// A matcher that asserts that a value is a [VariableNode] with the given 287 | /// [name]. 288 | Matcher _isVar(String name) => predicate( 289 | (dynamic value) => value is VariableNode && value.name == name, 290 | 'is a variable named "$name"'); 291 | 292 | void _expectToString(String selector, [String? result]) { 293 | result ??= selector; 294 | expect(_toString(selector), equals(result), 295 | reason: 'Expected toString of "$selector" to be "$result".'); 296 | } 297 | 298 | String _toString(String selector) => Parser(selector).parse().toString(); 299 | -------------------------------------------------------------------------------- /test/scanner_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/src/scanner.dart'; 6 | import 'package:boolean_selector/src/token.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | /// A matcher that asserts that a value is a [IdentifierToken]. 10 | const _isIdentifierToken = TypeMatcher(); 11 | 12 | void main() { 13 | group('peek()', () { 14 | test('returns the next token without consuming it', () { 15 | var scanner = Scanner('( )'); 16 | expect(scanner.peek().type, equals(TokenType.leftParen)); 17 | expect(scanner.peek().type, equals(TokenType.leftParen)); 18 | expect(scanner.peek().type, equals(TokenType.leftParen)); 19 | }); 20 | 21 | test('returns an end-of-file token at the end of a file', () { 22 | var scanner = Scanner('( )'); 23 | scanner.next(); 24 | scanner.next(); 25 | 26 | var token = scanner.peek(); 27 | expect(token.type, equals(TokenType.endOfFile)); 28 | expect(token.span.start.offset, equals(3)); 29 | expect(token.span.end.offset, equals(3)); 30 | }); 31 | 32 | test('throws a StateError if called after end-of-file was consumed', () { 33 | var scanner = Scanner('( )'); 34 | scanner.next(); 35 | scanner.next(); 36 | scanner.next(); 37 | expect(() => scanner.peek(), throwsStateError); 38 | }); 39 | }); 40 | 41 | group('next()', () { 42 | test('consumes and returns the next token', () { 43 | var scanner = Scanner('( )'); 44 | expect(scanner.next().type, equals(TokenType.leftParen)); 45 | expect(scanner.peek().type, equals(TokenType.rightParen)); 46 | expect(scanner.next().type, equals(TokenType.rightParen)); 47 | }); 48 | 49 | test('returns an end-of-file token at the end of a file', () { 50 | var scanner = Scanner('( )'); 51 | scanner.next(); 52 | scanner.next(); 53 | 54 | var token = scanner.next(); 55 | expect(token.type, equals(TokenType.endOfFile)); 56 | expect(token.span.start.offset, equals(3)); 57 | expect(token.span.end.offset, equals(3)); 58 | }); 59 | 60 | test('throws a StateError if called after end-of-file was consumed', () { 61 | var scanner = Scanner('( )'); 62 | scanner.next(); 63 | scanner.next(); 64 | scanner.next(); 65 | expect(() => scanner.next(), throwsStateError); 66 | }); 67 | }); 68 | 69 | group('scan()', () { 70 | test('consumes a matching token and returns true', () { 71 | var scanner = Scanner('( )'); 72 | expect(scanner.scan(TokenType.leftParen), isTrue); 73 | expect(scanner.peek().type, equals(TokenType.rightParen)); 74 | }); 75 | 76 | test("doesn't consume a matching token and returns false", () { 77 | var scanner = Scanner('( )'); 78 | expect(scanner.scan(TokenType.questionMark), isFalse); 79 | expect(scanner.peek().type, equals(TokenType.leftParen)); 80 | }); 81 | 82 | test('throws a StateError called after end-of-file was consumed', () { 83 | var scanner = Scanner('( )'); 84 | scanner.next(); 85 | scanner.next(); 86 | scanner.next(); 87 | expect(() => scanner.scan(TokenType.endOfFile), throwsStateError); 88 | }); 89 | }); 90 | 91 | group('scans a simple token:', () { 92 | test('left paren', () => _expectSimpleScan('(', TokenType.leftParen)); 93 | test('right paren', () => _expectSimpleScan(')', TokenType.rightParen)); 94 | test('or', () => _expectSimpleScan('||', TokenType.or)); 95 | test('and', () => _expectSimpleScan('&&', TokenType.and)); 96 | test('not', () => _expectSimpleScan('!', TokenType.not)); 97 | test('question mark', () => _expectSimpleScan('?', TokenType.questionMark)); 98 | test('colon', () => _expectSimpleScan(':', TokenType.colon)); 99 | }); 100 | 101 | group('scans an identifier that', () { 102 | test('is simple', () { 103 | var token = _scan(' foo '); 104 | expect(token, _isIdentifierToken); 105 | token as IdentifierToken; // promote token 106 | 107 | expect(token.name, equals('foo')); 108 | expect(token.span.text, equals('foo')); 109 | expect(token.span.start.offset, equals(3)); 110 | expect(token.span.end.offset, equals(6)); 111 | }); 112 | 113 | test('is a single character', () { 114 | var token = _scan('f'); 115 | expect(token, _isIdentifierToken); 116 | expect((token as IdentifierToken).name, equals('f')); 117 | }); 118 | 119 | test('has a leading underscore', () { 120 | var token = _scan('_foo'); 121 | expect(token, _isIdentifierToken); 122 | expect((token as IdentifierToken).name, equals('_foo')); 123 | }); 124 | 125 | test('has a leading dash', () { 126 | var token = _scan('-foo'); 127 | expect(token, _isIdentifierToken); 128 | expect((token as IdentifierToken).name, equals('-foo')); 129 | }); 130 | 131 | test('contains an underscore', () { 132 | var token = _scan('foo_bar'); 133 | expect(token, _isIdentifierToken); 134 | expect((token as IdentifierToken).name, equals('foo_bar')); 135 | }); 136 | 137 | test('contains a dash', () { 138 | var token = _scan('foo-bar'); 139 | expect(token, _isIdentifierToken); 140 | expect((token as IdentifierToken).name, equals('foo-bar')); 141 | }); 142 | 143 | test('is capitalized', () { 144 | var token = _scan('FOO'); 145 | expect(token, _isIdentifierToken); 146 | expect((token as IdentifierToken).name, equals('FOO')); 147 | }); 148 | 149 | test('contains numbers', () { 150 | var token = _scan('foo123'); 151 | expect(token, _isIdentifierToken); 152 | expect((token as IdentifierToken).name, equals('foo123')); 153 | }); 154 | }); 155 | 156 | test('scans an empty selector', () { 157 | expect(_scan('').type, equals(TokenType.endOfFile)); 158 | }); 159 | 160 | test('scans multiple tokens', () { 161 | var scanner = Scanner('(foo && bar)'); 162 | 163 | var token = scanner.next(); 164 | expect(token.type, equals(TokenType.leftParen)); 165 | expect(token.span.start.offset, equals(0)); 166 | expect(token.span.end.offset, equals(1)); 167 | 168 | token = scanner.next(); 169 | expect(token.type, equals(TokenType.identifier)); 170 | expect((token as IdentifierToken).name, equals('foo')); 171 | expect(token.span.start.offset, equals(1)); 172 | expect(token.span.end.offset, equals(4)); 173 | 174 | token = scanner.next(); 175 | expect(token.type, equals(TokenType.and)); 176 | expect(token.span.start.offset, equals(5)); 177 | expect(token.span.end.offset, equals(7)); 178 | 179 | token = scanner.next(); 180 | expect(token.type, equals(TokenType.identifier)); 181 | expect((token as IdentifierToken).name, equals('bar')); 182 | expect(token.span.start.offset, equals(8)); 183 | expect(token.span.end.offset, equals(11)); 184 | 185 | token = scanner.next(); 186 | expect(token.type, equals(TokenType.rightParen)); 187 | expect(token.span.start.offset, equals(11)); 188 | expect(token.span.end.offset, equals(12)); 189 | 190 | token = scanner.next(); 191 | expect(token.type, equals(TokenType.endOfFile)); 192 | expect(token.span.start.offset, equals(12)); 193 | expect(token.span.end.offset, equals(12)); 194 | }); 195 | 196 | group('ignores', () { 197 | test('a single-line comment', () { 198 | var scanner = Scanner('( // &&\n// ||\n)'); 199 | expect(scanner.next().type, equals(TokenType.leftParen)); 200 | expect(scanner.next().type, equals(TokenType.rightParen)); 201 | expect(scanner.next().type, equals(TokenType.endOfFile)); 202 | }); 203 | 204 | test('a single-line comment without a trailing newline', () { 205 | var scanner = Scanner('( // &&'); 206 | expect(scanner.next().type, equals(TokenType.leftParen)); 207 | expect(scanner.next().type, equals(TokenType.endOfFile)); 208 | }); 209 | 210 | test('a multi-line comment', () { 211 | var scanner = Scanner('( /* && * /\n|| */\n)'); 212 | expect(scanner.next().type, equals(TokenType.leftParen)); 213 | expect(scanner.next().type, equals(TokenType.rightParen)); 214 | expect(scanner.next().type, equals(TokenType.endOfFile)); 215 | }); 216 | 217 | test('a multi-line nested comment', () { 218 | var scanner = Scanner('(/* && /* ? /* || */ : */ ! */)'); 219 | expect(scanner.next().type, equals(TokenType.leftParen)); 220 | expect(scanner.next().type, equals(TokenType.rightParen)); 221 | expect(scanner.next().type, equals(TokenType.endOfFile)); 222 | }); 223 | 224 | test("Dart's notion of whitespace", () { 225 | var scanner = Scanner('( \t \n)'); 226 | expect(scanner.next().type, equals(TokenType.leftParen)); 227 | expect(scanner.next().type, equals(TokenType.rightParen)); 228 | expect(scanner.next().type, equals(TokenType.endOfFile)); 229 | }); 230 | }); 231 | 232 | group('disallows', () { 233 | test('a single |', () { 234 | expect(() => _scan('|'), throwsFormatException); 235 | }); 236 | 237 | test('"| |"', () { 238 | expect(() => _scan('| |'), throwsFormatException); 239 | }); 240 | 241 | test('a single &', () { 242 | expect(() => _scan('&'), throwsFormatException); 243 | }); 244 | 245 | test('"& &"', () { 246 | expect(() => _scan('& &'), throwsFormatException); 247 | }); 248 | 249 | test('an unknown operator', () { 250 | expect(() => _scan('=='), throwsFormatException); 251 | }); 252 | 253 | test('unicode', () { 254 | expect(() => _scan('öh'), throwsFormatException); 255 | }); 256 | 257 | test('an unclosed multi-line comment', () { 258 | expect(() => _scan('/*'), throwsFormatException); 259 | }); 260 | 261 | test('an unopened multi-line comment', () { 262 | expect(() => _scan('*/'), throwsFormatException); 263 | }); 264 | }); 265 | } 266 | 267 | /// Asserts that the first token scanned from [selector] has type [type], 268 | /// and that that token's span is exactly [selector]. 269 | void _expectSimpleScan(String selector, TokenType type) { 270 | // Complicate the selector to test that the span covers it correctly. 271 | var token = _scan(' $selector '); 272 | expect(token.type, equals(type)); 273 | expect(token.span.text, equals(selector)); 274 | expect(token.span.start.offset, equals(3)); 275 | expect(token.span.end.offset, equals(3 + selector.length)); 276 | } 277 | 278 | /// Scans a single token from [selector]. 279 | Token _scan(String selector) => Scanner(selector).next(); 280 | -------------------------------------------------------------------------------- /test/to_string_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/boolean_selector.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('toString() for', () { 10 | test('a variable is its name', () { 11 | _expectToString('foo'); 12 | _expectToString('a-b'); 13 | }); 14 | 15 | group('not', () { 16 | test("doesn't parenthesize a variable", () => _expectToString('!a')); 17 | test("doesn't parenthesize a nested not", () => _expectToString('!!a')); 18 | test('parenthesizes an or', () => _expectToString('!(a || b)')); 19 | test('parenthesizes an and', () => _expectToString('!(a && b)')); 20 | test('parenthesizes a condition', () => _expectToString('!(a ? b : c)')); 21 | }); 22 | 23 | group('or', () { 24 | test("doesn't parenthesize variables", () => _expectToString('a || b')); 25 | test("doesn't parenthesize nots", () => _expectToString('!a || !b')); 26 | 27 | test("doesn't parenthesize ors", () { 28 | _expectToString('a || b || c || d'); 29 | _expectToString('((a || b) || c) || d', 'a || b || c || d'); 30 | }); 31 | 32 | test('parenthesizes ands', 33 | () => _expectToString('a && b || c && d', '(a && b) || (c && d)')); 34 | 35 | test('parenthesizes conditions', 36 | () => _expectToString('(a ? b : c) || (e ? f : g)')); 37 | }); 38 | 39 | group('and', () { 40 | test("doesn't parenthesize variables", () => _expectToString('a && b')); 41 | test("doesn't parenthesize nots", () => _expectToString('!a && !b')); 42 | 43 | test( 44 | 'parenthesizes ors', 45 | () => 46 | _expectToString('(a || b) && (c || d)', '(a || b) && (c || d)')); 47 | 48 | test("doesn't parenthesize ands", () { 49 | _expectToString('a && b && c && d'); 50 | _expectToString('((a && b) && c) && d', 'a && b && c && d'); 51 | }); 52 | 53 | test('parenthesizes conditions', 54 | () => _expectToString('(a ? b : c) && (e ? f : g)')); 55 | }); 56 | 57 | group('conditional', () { 58 | test( 59 | "doesn't parenthesize variables", () => _expectToString('a ? b : c')); 60 | 61 | test("doesn't parenthesize nots", () => _expectToString('!a ? !b : !c')); 62 | 63 | test("doesn't parenthesize ors", 64 | () => _expectToString('a || b ? c || d : e || f')); 65 | 66 | test("doesn't parenthesize ands", 67 | () => _expectToString('a && b ? c && d : e && f')); 68 | 69 | test('parenthesizes non-trailing conditions', () { 70 | _expectToString('(a ? b : c) ? (e ? f : g) : h ? i : j'); 71 | _expectToString('(a ? b : c) ? (e ? f : g) : (h ? i : j)', 72 | '(a ? b : c) ? (e ? f : g) : h ? i : j'); 73 | }); 74 | }); 75 | }); 76 | } 77 | 78 | void _expectToString(String selector, [String? result]) { 79 | result ??= selector; 80 | expect(_toString(selector), equals(result), 81 | reason: 'Expected toString of "$selector" to be "$result".'); 82 | } 83 | 84 | String _toString(String selector) => BooleanSelector.parse(selector).toString(); 85 | -------------------------------------------------------------------------------- /test/validate_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/boolean_selector.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | var _selector = BooleanSelector.parse('foo && bar && baz'); 9 | 10 | void main() { 11 | test('throws if any variables are undefined', () { 12 | expect(() => _selector.validate((variable) => variable == 'bar'), 13 | throwsFormatException); 14 | }); 15 | 16 | test("doesn't throw if all variables are defined", () { 17 | // Should not throw. 18 | _selector.validate((variable) => true); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/variables_test.dart: -------------------------------------------------------------------------------- 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:boolean_selector/boolean_selector.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('a variable reports itself', () { 10 | expect(BooleanSelector.parse('foo').variables, equals(['foo'])); 11 | }); 12 | 13 | test('a negation reports its contents', () { 14 | expect(BooleanSelector.parse('!foo').variables, equals(['foo'])); 15 | }); 16 | 17 | test('a parenthesized expression reports its contents', () { 18 | expect(BooleanSelector.parse('(foo)').variables, equals(['foo'])); 19 | }); 20 | 21 | test('an or reports its contents', () { 22 | expect( 23 | BooleanSelector.parse('foo || bar').variables, equals(['foo', 'bar'])); 24 | }); 25 | 26 | test('an and reports its contents', () { 27 | expect( 28 | BooleanSelector.parse('foo && bar').variables, equals(['foo', 'bar'])); 29 | }); 30 | 31 | test('a conditional reports its contents', () { 32 | expect(BooleanSelector.parse('foo ? bar : baz').variables, 33 | equals(['foo', 'bar', 'baz'])); 34 | }); 35 | 36 | test('BooleanSelector.all reports no variables', () { 37 | expect(BooleanSelector.all.variables, isEmpty); 38 | }); 39 | 40 | test('BooleanSelector.none reports no variables', () { 41 | expect(BooleanSelector.none.variables, isEmpty); 42 | }); 43 | } 44 | --------------------------------------------------------------------------------