├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yaml ├── .gitignore ├── .pubignore ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── analysis_options.yaml ├── bin └── dart_dev.dart ├── dart_dependency_validator.yaml ├── dart_test.yaml ├── doc ├── README.md ├── tool-composition.md ├── tools │ ├── analyze-tool.md │ ├── format-tool.md │ ├── test-tool.md │ ├── tuneup-check-tool.md │ └── webdev-serve-tool.md └── v3-upgrade-guide.md ├── images ├── file_watcher_config.png └── file_watcher_pane.png ├── lib ├── dart_dev.dart ├── events.dart ├── src │ ├── core_config.dart │ ├── dart_dev_runner.dart │ ├── dart_dev_tool.dart │ ├── events.dart │ ├── executable.dart │ ├── tools │ │ ├── analyze_tool.dart │ │ ├── clean_tool.dart │ │ ├── compound_tool.dart │ │ ├── format_tool.dart │ │ ├── function_tool.dart │ │ ├── over_react_format_tool.dart │ │ ├── process_tool.dart │ │ ├── test_tool.dart │ │ ├── tuneup_check_tool.dart │ │ └── webdev_serve_tool.dart │ └── utils │ │ ├── arg_results_utils.dart │ │ ├── assert_dir_is_dart_package.dart │ │ ├── assert_no_args_after_separator.dart │ │ ├── assert_no_positional_args_before_separator.dart │ │ ├── assert_no_positional_args_nor_args_after_separator.dart │ │ ├── cached_pubspec.dart │ │ ├── dart_dev_paths.dart │ │ ├── dart_semver_version.dart │ │ ├── dart_tool_cache.dart │ │ ├── ensure_process_exit.dart │ │ ├── executables.dart │ │ ├── exit_process_signals.dart │ │ ├── format_tool_builder.dart │ │ ├── get_dart_version_comment.dart │ │ ├── global_package_is_active_and_compatible.dart │ │ ├── has_any_positional_args_before_separator.dart │ │ ├── has_args_after_separator.dart │ │ ├── logging.dart │ │ ├── organize_directives │ │ ├── namespace.dart │ │ ├── namespace_collector.dart │ │ ├── organize_directives.dart │ │ └── organize_directives_in_paths.dart │ │ ├── package_is_immediate_dependency.dart │ │ ├── parse_flag_from_args.dart │ │ ├── parse_imports.dart │ │ ├── process_declaration.dart │ │ ├── pubspec_lock.dart │ │ ├── rest_args_with_separator.dart │ │ ├── run_process_and_ensure_exit.dart │ │ ├── start_process_and_ensure_exit.dart │ │ ├── verbose_enabled.dart │ │ └── version.dart └── utils.dart ├── pubspec.yaml ├── test ├── functional.dart ├── functional │ ├── analyze_tool_functional_test.dart │ ├── documentation_test.dart │ ├── fixtures │ │ ├── analysis_options.yaml │ │ ├── analyze │ │ │ ├── failure │ │ │ │ ├── lib │ │ │ │ │ └── lib.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── tool │ │ │ │ │ └── dev.dart │ │ │ └── success │ │ │ │ ├── lib │ │ │ │ └── lib.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── tool │ │ │ │ └── dev.dart │ │ ├── format │ │ │ └── unsorted_imports │ │ │ │ ├── organize_directives_off │ │ │ │ ├── lib │ │ │ │ │ └── main.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── tool │ │ │ │ │ └── dart_dev │ │ │ │ │ └── config.dart │ │ │ │ └── organize_directives_on │ │ │ │ ├── lib │ │ │ │ └── main.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── tool │ │ │ │ └── dart_dev │ │ │ │ └── config.dart │ │ └── null_safety │ │ │ ├── opted_in_custom_config │ │ │ ├── lib │ │ │ │ └── lib.dart │ │ │ ├── pubspec.yaml │ │ │ └── tool │ │ │ │ └── dart_dev │ │ │ │ └── config.dart │ │ │ ├── opted_in_custom_config_version_comment │ │ │ ├── lib │ │ │ │ └── lib.dart │ │ │ ├── pubspec.yaml │ │ │ └── tool │ │ │ │ └── dart_dev │ │ │ │ └── config.dart │ │ │ └── opted_in_no_config │ │ │ ├── lib │ │ │ └── lib.dart │ │ │ └── pubspec.yaml │ ├── format_tool_functional_test.dart │ └── null_safety_functional_test.dart ├── log_matchers.dart ├── tools │ ├── analyze_tool_test.dart │ ├── compound_tool_test.dart │ ├── fixtures │ │ ├── analysis_options.yaml │ │ ├── analyze │ │ │ ├── failing │ │ │ │ └── failing.dart │ │ │ ├── globs │ │ │ │ ├── file.dart │ │ │ │ └── file.txt │ │ │ └── passing │ │ │ │ └── passing.dart │ │ ├── format │ │ │ ├── globs │ │ │ │ ├── .dart_tool_test │ │ │ │ │ └── file.dart │ │ │ │ ├── file.dart │ │ │ │ ├── file.txt │ │ │ │ ├── lib │ │ │ │ │ └── sub │ │ │ │ │ │ └── file.dart │ │ │ │ ├── linked.dart │ │ │ │ ├── links │ │ │ │ │ ├── lib-link │ │ │ │ │ ├── link.dart │ │ │ │ │ └── not_link.dart │ │ │ │ ├── other │ │ │ │ │ └── file.dart │ │ │ │ └── should_exclude.dart │ │ │ ├── has_dart_style │ │ │ │ └── pubspec.yaml │ │ │ └── missing_dart_style │ │ │ │ └── pubspec.yaml │ │ ├── organize_directives │ │ │ └── directives.dart │ │ └── tuneup_check │ │ │ ├── has_tuneup │ │ │ └── pubspec.yaml │ │ │ └── missing_tuneup │ │ │ └── pubspec.yaml │ ├── format_tool_test.dart │ ├── function_tool_test.dart │ ├── process_tool_test.dart │ ├── shared_tool_tests.dart │ ├── test_tool_test.dart │ ├── tuneup_check_tool_test.dart │ └── webdev_serve_tool_test.dart ├── utils.dart └── utils │ ├── arg_results_utils_test.dart │ ├── assert_no_positional_args_before_separator_test.dart │ ├── dart_dev_paths_test.dart │ ├── format_tool_builder_test.dart │ ├── get_dart_version_comment_test.dart │ ├── organize_imports │ └── organize_directives_test.dart │ ├── parse_imports_test.dart │ └── rest_args_with_separator_test.dart └── tool ├── dart_dev └── config.dart └── file_watchers └── ddev_format_on_save.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Workiva/fedx 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | groups: 8 | gha: 9 | patterns: ["*"] 10 | 11 | - package-ecosystem: pub 12 | directory: / 13 | schedule: 14 | interval: weekly 15 | groups: 16 | major: 17 | update-types: ["major"] 18 | minor: 19 | update-types: ["minor", "patch"] -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'master' 8 | - 'test_consume_*' 9 | 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | id-token: write 14 | 15 | jobs: 16 | build: 17 | uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.7 18 | 19 | dart: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: [ ubuntu, windows ] 24 | sdk: [ 2.19.6, stable ] 25 | name: Dart ${{ matrix.sdk }} on ${{ matrix.os }} 26 | runs-on: ${{ matrix.os }}-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dart-lang/setup-dart@v1 30 | with: 31 | sdk: ${{ matrix.sdk }} 32 | - name: Install dependencies 33 | run: dart pub get 34 | - name: Validate dependencies 35 | run: dart run dependency_validator 36 | - name: Analysis 37 | run: dart run dart_dev analyze 38 | - name: Formatting 39 | if: ${{ matrix.sdk == 'stable' && matrix.os == 'ubuntu' }} 40 | run: dart run dart_dev format --check 41 | - name: Tests 42 | run: dart run dart_dev test ${{ matrix.sdk != '2.19.6' && '--test-args="--exclude-tags dart2"' || '' }} -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | pull-requests: write 12 | 13 | jobs: 14 | publish: 15 | uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .idea/ 3 | .packages 4 | .pub/ 5 | build/ 6 | /doc/api/ 7 | /pubspec.lock 8 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | skynet.yaml 3 | .dart_tool/ 4 | .idea/ 5 | .packages 6 | .pub/ 7 | build/ 8 | /doc/api/ 9 | /pubspec.lock 10 | test/ 11 | tool/ -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | dart_dev 2 | Copyright 2015-2024 Workiva Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | -------------------------------------------------- 17 | 18 | This dart_dev software includes a number of subcomponents with 19 | separate copyright notices and/or license terms. Your use of the source 20 | code for the these subcomponents is subject to the terms and 21 | conditions of the following licenses: 22 | 23 | Dart-lang build software: https://github.com/dart-lang/build 24 | Copyright ©2016 Dart project authors. 25 | Licensed under the BSD 3-Clause License: https://github.com/dart-lang/build/blob/master/LICENSE 26 | 27 | Dart-lang build software: https://github.com/dart-lang/test 28 | Copyright ©2016 Dart project authors. 29 | Licensed under the BSD 3-Clause License: https://github.com/dart-lang/test/blob/master/pkgs/test/LICENSE 30 | 31 | ----------------------------------------------- 32 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - test/**/fixtures/** 6 | language: 7 | strict-inference: true 8 | strict-raw-types: true 9 | strong-mode: 10 | implicit-casts: true 11 | implicit-dynamic: true 12 | 13 | linter: 14 | rules: 15 | include: 16 | - avoid_types_on_closure_parameters 17 | - prefer_void_to_null 18 | - void_checks 19 | exclude: 20 | - overridden_fields 21 | -------------------------------------------------------------------------------- /bin/dart_dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/src/executable.dart' as executable; 2 | 3 | void main(List args) => executable.run(args); 4 | -------------------------------------------------------------------------------- /dart_dependency_validator.yaml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - test/**/fixtures/**.dart 3 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | platforms: 2 | - vm 3 | 4 | timeout: 2s 5 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The `dart_dev` package includes: 4 | 5 | - Tools for running developer tasks via the `dart_dev` executable. 6 | - Utils for composing tools together. 7 | - Utils for creating your own tools. 8 | - A shared config that serves as a recommended starting place for your 9 | `tool/dart_dev/config.dart`. 10 | 11 | --- 12 | --- 13 | 14 | 15 | 16 | - Tools 17 | - [`AnalyzeTool`][analyze-tool] 18 | - [`FormatTool`][format-tool] 19 | - [`TestTool`][test-tool] 20 | - [`TuneupCheckTool`][tuneup-check-tool] 21 | - [`WebdevServeTool`][webdev-serve-tool] 22 | - [Creating, Extending, and Composing Tools][tool-composition] 23 | - [v3 upgrade guide][v3-upgrade-guide] 24 | 25 | 26 | [analyze-tool]: /doc/tools/analyze-tool.md 27 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 28 | [dart-function-tool]: /doc/tools/dart-function-tool.md 29 | [format-tool]: /doc/tools/format-tool.md 30 | [process-tool]: /doc/tools/process-tool.md 31 | [test-tool]: /doc/tools/test-tool.md 32 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 33 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 34 | [tool-composition]: /doc/tool-composition.md 35 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 36 | -------------------------------------------------------------------------------- /doc/tool-composition.md: -------------------------------------------------------------------------------- 1 | # Creating, Extending, and Composing Tools 2 | 3 | ## Create a tool from a function 4 | 5 | ```dart 6 | // tool/dart_dev/config.dart 7 | import 'package:dart_dev/dart_dev.dart'; 8 | import 'package:io/io.dart'; 9 | 10 | int hello([DevToolExecutionContext context]) { 11 | print('Hello!'); 12 | return ExitCode.success.code; 13 | } 14 | 15 | final config = { 16 | 'hello': DevTool.fromFunction(hello), 17 | }; 18 | ``` 19 | 20 | ```bash 21 | $ ddev hello 22 | Hello! 23 | ``` 24 | 25 | ### Handling command-line args 26 | 27 | ```dart 28 | // tool/dart_dev/config.dart 29 | import 'package:args/args.dart'; 30 | import 'package:dart_dev/dart_dev.dart'; 31 | import 'package:io/io.dart'; 32 | 33 | final helloArgParser = ArgParser() 34 | ..addOption('name', help: 'Your name.'); 35 | 36 | int hello([DevToolExecutionContext context]) { 37 | var name; 38 | if (context?.argResults != null) { 39 | name = context.argResults['name']; 40 | } 41 | print('Hello${name != null ? ', $name' : ''}!'); 42 | return ExitCode.success.code; 43 | } 44 | 45 | final config = { 46 | 'hello': DevTool.fromFunction(hello, argParser: helloArgParser), 47 | }; 48 | ``` 49 | 50 | ```bash 51 | $ ddev hello --name Dart 52 | Hello, Dart! 53 | ``` 54 | 55 | ## Create a tool that runs a subprocess 56 | 57 | ```dart 58 | // tool/dart_dev/config.dart 59 | import 'package:dart_dev/dart_dev.dart'; 60 | 61 | final config = { 62 | 'github': DevTool.fromProcess( 63 | 'open', ['https://github.com/Workiva/dart_dev']), 64 | }; 65 | ``` 66 | 67 | ### Using command-line args with a subprocess tool 68 | 69 | If you want to dynamically build a subprocess or run it conditionally based on 70 | command-line args, you can compose the function and process tools like so: 71 | 72 | ```dart 73 | // tool/dart_dev/config.dart 74 | import 'dart:async'; 75 | 76 | import 'package:args/args.dart'; 77 | import 'package:dart_dev/dart_dev.dart'; 78 | import 'package:io/io.dart'; 79 | 80 | final githubArgParser = ArgParser()..addFlag('open'); 81 | 82 | FutureOr github([DevToolExecutionContext context]) async { 83 | final url = 'https://github.com/Workiva/dart_dev'; 84 | final shouldOpen = context?.argResults != null && context.argResults['open']; 85 | if (shouldOpen) { 86 | return DevTool.fromProcess('open', [url]).run(); 87 | } 88 | print(url); 89 | return ExitCode.success.code; 90 | } 91 | 92 | final config = { 93 | 'github': DevTool.fromFunction(github, argParser: githubArgParser), 94 | }; 95 | ``` 96 | 97 | ```bash 98 | $ ddev github 99 | https://github.com/Workiva/dart_dev 100 | 101 | $ ddev github --open 102 | # opens https://github.com/Workiva/dart_dev in browser 103 | ``` 104 | 105 | ## `CompoundTool` 106 | 107 | `CompoundTool` is designed to make it easy to compose multiple tools into a 108 | single tool that can be run via a single `ddev` target. 109 | 110 | Additionally, each tool added to a `CompoundTool` can be configured with one of 111 | two possible run conditions: 112 | 113 | 1. When passing: run only when all previous tools have succeeded (**default**) 114 | 2. Always: run regardless of the success/failure of previous tools 115 | 116 | Configuring a tool to always run is useful for set-up and tear-down tools, like 117 | starting and stopping a test server before and after running tests, 118 | respectively. 119 | 120 | ```dart 121 | // tool/dart_dev/config.dart 122 | import 'package:dart_dev/dart_dev.dart'; 123 | import 'package:io/io.dart'; 124 | 125 | int startServer([DevToolExecutionContext context]) { 126 | // Start server and wait for it to be ready... 127 | return ExitCode.success.code; 128 | } 129 | 130 | int stopServer([DevToolExecutionContext context]) { 131 | // Stop server... 132 | return ExitCode.success.code; 133 | } 134 | 135 | final config = { 136 | 'test': CompoundTool() 137 | ..addTool(DevTool.fromFunction(startServer), alwaysRun: true) 138 | ..addTool(TestTool()) 139 | ..addTool(DevTool.fromFunction(stopServer), alwaysRun: true), 140 | }; 141 | ``` 142 | 143 | ### `BackgroundProcessTool` 144 | 145 | The `BackgroundProcessTool` can be used in conjunction with `CompoundTool` to 146 | wrap a tool with the starting and stopping of a background subprocess: 147 | 148 | ```dart 149 | final testServer = BackgroundProcessTool( 150 | 'node', ['tool/server.js'], 151 | delayAfterStart: Duration(seconds: 1)); 152 | 153 | final config = { 154 | 'test': CompoundTool() 155 | ..addTool(testServer.starter, alwaysRun: true) 156 | ..addTool(TestTool()) 157 | ..addTool(testServer.stopper, alwaysRun: true), 158 | }; 159 | ``` 160 | 161 | ### Mapping args to tools 162 | 163 | `CompoundTool.addTool()` supports an optional `argMapper` parameter that can be 164 | used to customize the `ArgResults` instance that the tool gets when it runs. 165 | 166 | The typedef for this `argMapper` function is: 167 | 168 | ```dart 169 | typedef ArgMapper = ArgResults Function(ArgParser parser, ArgResults results); 170 | ``` 171 | 172 | By default, subtools added to a `CompoundTool` will _only_ receive option args 173 | that are defined by their respective `ArgParser`: 174 | 175 | ```dart 176 | // tool/dart_dev/config.dart 177 | import 'package:dart_dev/dart_dev.dart'; 178 | 179 | final config = { 180 | 'example': CompoundTool() 181 | // This subtool has an ArgParser that only supports the --foo flag. 182 | ..addTool(DevTool.fromFunction((_) => 0, 183 | argParser: ArgParser()..addFlag('foo'))) 184 | 185 | // This subtool has an ArgParser that only supports the --bar flag. 186 | ..addTool(DevTool.fromFunction((_) => 0, 187 | argParser: ArgParser()..addFlag('bar'))) 188 | }; 189 | ``` 190 | 191 | With the above configuration, running `ddev example --foo --bar` will result in 192 | the compound tool running the first subtool with only the `--foo` option 193 | followed by the second subtool with only the `--bar` option. Any positional args 194 | would be discarded. 195 | 196 | You may want one of the subtools to also receive the positional args. To 197 | illustrate this, our test tool example from above can be updated to allow 198 | positional args to be sent to the `TestTool` portion so that individual test 199 | files can be targeted. 200 | 201 | To do this, we can use the `takeAllArgs` function provided by dart_dev: 202 | 203 | ```dart 204 | // tool/dart_dev/config.dart 205 | import 'package:dart_dev/dart_dev.dart'; 206 | 207 | final config = { 208 | 'test': CompoundTool() 209 | ..addTool(DevTool.fromFunction(startServer), alwaysRun: true) 210 | // Using `takeAllArgs` on this subtool will allow it to receive 211 | // the positional args passed to `ddev test` as well as any 212 | // option args specific to the `TestTool`. 213 | ..addTool(TestTool(), argMapper: takeAllArgs) 214 | ..addTool(DevTool.fromFunction(stopServer), alwaysRun: true), 215 | }; 216 | 217 | int startServer([DevToolExecutionContext context]) => 0; 218 | int stopServer([DevToolExecutionContext context]) => 0; 219 | ``` 220 | 221 | The default behavior for subtools along with using `takeAllArgs` for the subtool 222 | that needs the positional args should cover most use cases. However, you may 223 | write your own `ArgMapper` function if further customization is needed. 224 | 225 | ### Sharing state across tools 226 | 227 | With more complex use cases, it may be necessary to share or use state across 228 | the individual tools that make up a compound tool. To accomplish this, you can 229 | either create a closure or a class within which to share said state. 230 | 231 | ```dart 232 | // tool/dart_dev/config.dart 233 | import 'package:args/args.dart'; 234 | import 'package:dart_dev/dart_dev.dart'; 235 | import 'package:io/io.dart'; 236 | 237 | class NewAnalyzeTool extends AnalyzeTool with CompoundToolMixin { 238 | NewAnalyzeTool() { 239 | addTool( 240 | DevTool.fromFunction( 241 | _parseStrictMode, 242 | argParser: _strictModeArgParser, 243 | ), 244 | ); 245 | addTool( 246 | DevTool.fromFunction( 247 | _runAnalyzeTool, 248 | argParser: AnalyzeTool().argParser, 249 | ), 250 | ); 251 | } 252 | 253 | bool _strictModeEnabled; 254 | 255 | final _strictModeArgParser = ArgParser()..addFlag('strict'); 256 | 257 | int _parseStrictMode([DevToolExecutionContext context]) { 258 | _strictModeEnabled = context?.argResults != null && 259 | context.argResults['strict'] ?? false; 260 | return ExitCode.success.code; 261 | } 262 | 263 | Future _runAnalyzeTool([DevToolExecutionContext context]) { 264 | // Build an AnalyzeTool instance using this "NewAnalyzeTool" 265 | // instance as the base config. 266 | final analyzeTool = AnalyzeTool() 267 | ..analyzerArgs = analyzerArgs 268 | ..include = include; 269 | 270 | // Augment the configuration if strict mode is enabled. 271 | if (_strictModeEnabled) { 272 | (analyzeTool.analyzerArgs ??= []).addAll([ 273 | '--fatal-lints', 274 | '--fatal-infos', 275 | '--fatal-warnings', 276 | ]); 277 | } 278 | 279 | return analyzeTool.run(context); 280 | } 281 | } 282 | 283 | // Consume and configure the NewAnalyzeTool as if it were an instance of 284 | // the AnalyzeTool, when in reality it is a compound tool. 285 | final config = { 286 | 'analyze': NewAnalyzeTool() 287 | ..analyzerArgs = ['--no-implicit-dynamic'], 288 | }; 289 | ``` 290 | 291 | --- 292 | --- 293 | 294 | 295 | 296 | - Tools 297 | - [`AnalyzeTool`][analyze-tool] 298 | - [`FormatTool`][format-tool] 299 | - [`TestTool`][test-tool] 300 | - [`TuneupCheckTool`][tuneup-check-tool] 301 | - [`WebdevServeTool`][webdev-serve-tool] 302 | - [Creating, Extending, and Composing Tools][tool-composition] 303 | - [v3 upgrade guide][v3-upgrade-guide] 304 | 305 | 306 | [analyze-tool]: /doc/tools/analyze-tool.md 307 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 308 | [dart-function-tool]: /doc/tools/dart-function-tool.md 309 | [format-tool]: /doc/tools/format-tool.md 310 | [process-tool]: /doc/tools/process-tool.md 311 | [test-tool]: /doc/tools/test-tool.md 312 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 313 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 314 | [tool-composition]: /doc/tool-composition.md 315 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 316 | -------------------------------------------------------------------------------- /doc/tools/analyze-tool.md: -------------------------------------------------------------------------------- 1 | # `AnalyzeTool` 2 | 3 | Statically analyzes the current project by running the `dartanalyzer`. 4 | 5 | ## Usage 6 | 7 | > _This tool is included in the [`coreConfig`][core-config] and is runnable by 8 | > default via `ddev analyze`._ 9 | 10 | ```dart 11 | // tool/dart_dev/config.dart 12 | import 'package:dart_dev/dart_dev.dart'; 13 | 14 | final config = { 15 | 'analyze': AnalyzeTool() // configure as necessary 16 | }; 17 | ``` 18 | 19 | ## Default behavior 20 | 21 | By default this tool will run `dartanalyzer .` which will analyze all dart files 22 | in the current project. 23 | 24 | ## Configuration 25 | 26 | `AnalyzeTool` supports one configuration option which is the list of args to 27 | pass to the `dartanalyzer` process: 28 | 29 | ```dart 30 | // tool/dart_dev/config.dart 31 | import 'package:dart_dev/dart_dev.dart'; 32 | 33 | final config = { 34 | 'analyze': AnalyzeTool() 35 | ..analyzerArgs = ['--fatal-infos', '--fatal-warnings'] 36 | }; 37 | ``` 38 | 39 | > _Always prefer configuring the analyzer via `analysis_options.yaml` when 40 | > possible. This ensures that other tools that leverage the analyzer or the 41 | > analysis server benefit from the configuration, as well._ 42 | 43 | ## Excluding files from analysis 44 | 45 | The `analysis_options.yaml` configuration file 46 | [supports excluding files][analysis-exclude]. However, there is an 47 | [open issue with the `dartanalyzer` CLI][analyzer-exclude-issue] because it does 48 | not respect this list. 49 | 50 | If your project has files that need to be excluded from analysis (e.g. generated 51 | files), use the [`TuneupCheckTool`][tuneup-check-tool]. It uses the 52 | `tuneup` package to run analysis instead of `dartanalyzer` and it properly 53 | respects the exclude rules defined in `analysis_options.yaml`. 54 | 55 | ## Command-line options 56 | 57 | ```bash 58 | $ ddev help analyze 59 | ``` 60 | 61 | [analyzer-exclude-issue]: https://github.com/dart-lang/sdk/issues/25551 62 | [analysis-exclude]: https://dart.dev/guides/language/analysis-options#excluding-code-from-analysis 63 | [core-config]: /lib/src/core_config.dart 64 | 65 | --- 66 | --- 67 | 68 | 69 | 70 | - Tools 71 | - [`AnalyzeTool`][analyze-tool] 72 | - [`FormatTool`][format-tool] 73 | - [`TestTool`][test-tool] 74 | - [`TuneupCheckTool`][tuneup-check-tool] 75 | - [`WebdevServeTool`][webdev-serve-tool] 76 | - [Creating, Extending, and Composing Tools][tool-composition] 77 | - [v3 upgrade guide][v3-upgrade-guide] 78 | 79 | 80 | [analyze-tool]: /doc/tools/analyze-tool.md 81 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 82 | [dart-function-tool]: /doc/tools/dart-function-tool.md 83 | [format-tool]: /doc/tools/format-tool.md 84 | [process-tool]: /doc/tools/process-tool.md 85 | [test-tool]: /doc/tools/test-tool.md 86 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 87 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 88 | [tool-composition]: /doc/tool-composition.md 89 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 90 | -------------------------------------------------------------------------------- /doc/tools/format-tool.md: -------------------------------------------------------------------------------- 1 | # `FormatTool` 2 | 3 | Formats dart files in the current project by running `dartfmt`. 4 | 5 | ## Usage 6 | 7 | > _This tool is included in the [`coreConfig`][core-config] and is runnable by 8 | > default via `ddev format`._ 9 | 10 | ```dart 11 | // tool/dart_dev/config.dart 12 | import 'package:dart_dev/dart_dev.dart'; 13 | 14 | final config = { 15 | 'format': FormatTool() // configure as necessary 16 | }; 17 | ``` 18 | 19 | ## Default behavior 20 | 21 | By default this tool will run `dartfmt -w .` which will format all dart files in 22 | the current project. 23 | 24 | ## Configuration 25 | 26 | ### Default mode 27 | 28 | `FormatTool` can be run in 3 modes: 29 | 30 | - `FormatMode.overwrite` (default) 31 | - e.g. `ddev format -w` 32 | - `FormatMode.dryRun` (lists files that would be changed) 33 | - e.g. `ddev format -n` 34 | - `FormatMode.check` (dry-run _and_ sets the exit code if changes are needed) 35 | - e.g. `ddev format -c` 36 | 37 | ```dart 38 | // tool/dart_dev/config.dart 39 | import 'package:dart_dev/dart_dev.dart'; 40 | 41 | final config = { 42 | 'format': FormatTool() 43 | ..defaultMode = FormatMode.check 44 | }; 45 | ``` 46 | 47 | ### Using the `dart_style` package instead of `dartfmt` 48 | 49 | Some projects like to depend on a specific version of the `dart_style` package 50 | and use its `format` executable rather than the `dartfmt` provided by the Dart 51 | SDK. 52 | 53 | ```dart 54 | // tool/dart_dev/config.dart 55 | import 'package:dart_dev/dart_dev.dart'; 56 | 57 | final config = { 58 | 'format': FormatTool() 59 | ..formatter = Formatter.dartStyle 60 | }; 61 | ``` 62 | 63 | ### Passing args to the formatter process 64 | 65 | ```dart 66 | // tool/dart_dev/config.dart 67 | import 'package:dart_dev/dart_dev.dart'; 68 | 69 | final config = { 70 | 'format': FormatTool() 71 | ..formatterArgs = ['--fix'] 72 | }; 73 | ``` 74 | 75 | ```bash 76 | $ ddev format 77 | [INFO] Running subprocess... 78 | dartfmt -w --fix . 79 | ---------------------------- 80 | ``` 81 | 82 | ### Excluding files from formatting 83 | 84 | ```dart 85 | // tool/dart_dev/config.dart 86 | import 'package:dart_dev/dart_dev.dart'; 87 | import 'package:glob/glob.dart'; 88 | 89 | final config = { 90 | 'format': FormatTool() 91 | ..exclude = [Glob('test_fixtures/**'), Glob('**.g.dart')] 92 | }; 93 | ``` 94 | 95 | ### Organizing directives 96 | 97 | By default, the format tool will not sort imports/export. They can be automatically 98 | sorted by setting `organizeDirectives`. 99 | 100 | ```dart 101 | // tool/dart_dev/config.dart 102 | import 'package:dart_dev/dart_dev.dart'; 103 | 104 | final config = { 105 | 'format': FormatTool() 106 | ..organizeDirectives = true 107 | }; 108 | ``` 109 | 110 | ## Command-line options 111 | 112 | ```bash 113 | $ ddev help format 114 | ``` 115 | 116 | [core-config]: /lib/src/core_config.dart 117 | 118 | --- 119 | --- 120 | 121 | 122 | 123 | - Tools 124 | - [`AnalyzeTool`][analyze-tool] 125 | - [`FormatTool`][format-tool] 126 | - [`TestTool`][test-tool] 127 | - [`TuneupCheckTool`][tuneup-check-tool] 128 | - [`WebdevServeTool`][webdev-serve-tool] 129 | - [Creating, Extending, and Composing Tools][tool-composition] 130 | - [v3 upgrade guide][v3-upgrade-guide] 131 | 132 | 133 | [analyze-tool]: /doc/tools/analyze-tool.md 134 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 135 | [dart-function-tool]: /doc/tools/dart-function-tool.md 136 | [format-tool]: /doc/tools/format-tool.md 137 | [process-tool]: /doc/tools/process-tool.md 138 | [test-tool]: /doc/tools/test-tool.md 139 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 140 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 141 | [tool-composition]: /doc/tool-composition.md 142 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 143 | -------------------------------------------------------------------------------- /doc/tools/test-tool.md: -------------------------------------------------------------------------------- 1 | # `TestTool` 2 | 3 | Runs dart tests for the current project. 4 | 5 | ## Usage 6 | 7 | > _This tool is included in the [`coreConfig`][core-config] and is runnable by 8 | > default via `ddev test`._ 9 | 10 | ```dart 11 | // tool/dart_dev/config.dart 12 | import 'package:dart_dev/dart_dev.dart'; 13 | 14 | final config = { 15 | 'test': TestTool() // configure as necessary 16 | }; 17 | ``` 18 | 19 | ## `test` vs `build_runner test` 20 | 21 | Historically, `dart test` has been the canonical way to run Dart tests. 22 | With the introduction of the [build system][build-system], there is now a second 23 | way to run tests. Projects that rely on builder outputs must run tests via 24 | `dart run build_runner test`. 25 | 26 | The `TestTool` will make this choice for you. If the current project has a 27 | dependency on `build_test`, it will run `dart run build_runner test`. Otherwise 28 | it will default to running `dart test`. 29 | 30 | > It [appears][test-future] as though the long term goal is to integrate the 31 | > build system into the test runner so that `dart test` is once again the 32 | > canonical way to run tests. 33 | 34 | ## Default behavior 35 | 36 | By default this tool will run `dart test`, unless there is a dependency on 37 | `build_test`, in which case it will run `dart run build_runner test`. 38 | 39 | ## Running a subset of tests 40 | 41 | When developing it is common to want to run a targeted subset of tests. The test 42 | runner supports targeting tests by path(s), preset(s), or by matching 43 | against the test descriptions. These common command-line options are available 44 | when running the `TestTool` via `ddev test`, as well. 45 | 46 | ```bash 47 | $ ddev help test 48 | Run dart tests in this package. 49 | 50 | Usage: dart_dev test [files or directories...] 51 | ======== Selecting Tests 52 | -n, --name A substring of the name of the test to run. 53 | Regular expression syntax is supported. 54 | If passed multiple times, tests must match all substrings. 55 | 56 | -N, --plain-name A plain-text substring of the name of the test to run. 57 | If passed multiple times, tests must match all substrings. 58 | 59 | ======== Running Tests 60 | -P, --preset The configuration preset(s) to use. 61 | --[no-]release Build with release mode defaults for builders. 62 | This only applies in projects that run tests with build_runner. 63 | 64 | ======== Output 65 | --reporter The runner used to print test results. 66 | 67 | [compact] A single line, updated continuously. 68 | [expanded] A separate line for each update. 69 | [json] A machine-readable format (see https://goo.gl/gBsV1a). 70 | 71 | ======== Other Options 72 | --test-stdout Write the test process stdout to this file path. 73 | --test-args Args to pass to the test runner process. 74 | Run "dart test -h -v" to see all available options. 75 | 76 | --build-args Args to pass to the build runner process. 77 | Run "dart run build_runner test -h -v" to see all available options. 78 | Note: these args are only applicable if the current project depends on "build_test". 79 | 80 | -h, --help Print this usage information. 81 | ``` 82 | 83 | Additionally, in projects that use `build_runner` to run tests, the `TestTool` 84 | will automatically apply [build filters][build-filters] so that the build system 85 | _only_ builds the set of outputs necessary for running the targeted test paths. 86 | In large projects, this can significantly reduce the build time which makes 87 | iterating on tests much more efficient. 88 | 89 | ```bash 90 | $ ddev test test/foo/bar/ test/baz_test.dart 91 | [INFO] Running subprocess: 92 | dart run build_runner test --build-filter=test/foo/bar/** --build-filter=test/baz_test.dart.*_test.dart.js --build-filter=test/baz_test.html -- test/foo/bar/ test/baz_test.dart 93 | ---------------------------------------------------------------------------- 94 | ``` 95 | 96 | ## Collecting coverage 97 | 98 | The test package now has partial support for [coverage collection][coverage] 99 | built-in. As of now, it is only supported for tests run on the Dart VM. Follow 100 | [this issue][coverage-issue] for updates on implementing coverage collection for 101 | tests run in Chrome. 102 | 103 | There are plans to add a "coverage" mode to the `TestTool` that will pass the 104 | coverage directory to the test command and handle formatting the coverage output 105 | to a more consumable format like lcov. It will optionally generate and open an 106 | HTML report using the `genhtml` tool. 107 | 108 | ## Configuration 109 | 110 | > Always prefer configuring the test runner via 111 | > [`dart_test.yaml`][dart-test-yaml] when possible. This ensures that other 112 | > tools that leverage the test runner benefit from the configuration, as well. 113 | 114 | ### Passing args to the test process 115 | 116 | ```dart 117 | // tool/dart_dev/config.dart 118 | import 'package:dart_dev/dart_dev.dart'; 119 | 120 | final config = { 121 | 'test': TestTool() 122 | ..testArgs = ['--no-chain-stack-traces'] 123 | }; 124 | ``` 125 | 126 | ### Passing args to the build_runner process 127 | 128 | _Note that this is only applicable in projects that run tests via 129 | `build_runner`._ 130 | 131 | ```dart 132 | // tool/dart_dev/config.dart 133 | import 'package:dart_dev/dart_dev.dart'; 134 | 135 | final config = { 136 | 'test': TestTool() 137 | ..buildArgs = ['--delete-conflicting-outputs'] 138 | }; 139 | ``` 140 | 141 | ## Command-line options 142 | 143 | ```bash 144 | $ ddev help test 145 | ``` 146 | 147 | [build-filters]: https://github.com/dart-lang/build/blob/master/build_runner/CHANGELOG.md#new-feature-build-filters 148 | [build-system]: https://github.com/dart-lang/build 149 | [core-config]: /lib/src/core_config.dart 150 | [coverage]: https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#collecting-code-coverage 151 | [coverage-issue]: https://github.com/dart-lang/test/issues/36 152 | [dart-test-yaml]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md 153 | [test-future]: https://github.com/dart-lang/build/pull/2415#issuecomment-530114943 154 | 155 | --- 156 | --- 157 | 158 | 159 | 160 | - Tools 161 | - [`AnalyzeTool`][analyze-tool] 162 | - [`FormatTool`][format-tool] 163 | - [`TestTool`][test-tool] 164 | - [`TuneupCheckTool`][tuneup-check-tool] 165 | - [`WebdevServeTool`][webdev-serve-tool] 166 | - [Creating, Extending, and Composing Tools][tool-composition] 167 | - [v3 upgrade guide][v3-upgrade-guide] 168 | 169 | 170 | [analyze-tool]: /doc/tools/analyze-tool.md 171 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 172 | [dart-function-tool]: /doc/tools/dart-function-tool.md 173 | [format-tool]: /doc/tools/format-tool.md 174 | [process-tool]: /doc/tools/process-tool.md 175 | [test-tool]: /doc/tools/test-tool.md 176 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 177 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 178 | [tool-composition]: /doc/tool-composition.md 179 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 180 | -------------------------------------------------------------------------------- /doc/tools/tuneup-check-tool.md: -------------------------------------------------------------------------------- 1 | # `TuneupCheckTool` 2 | 3 | Statically analyzes the current project via the `tuneup` package. 4 | 5 | ## Usage 6 | 7 | This is intended to be used as a drop-in replacement to the 8 | [`AnalyzeTool`][analyze-tool] to workaround an 9 | [open issue with `dartanalyzer` and excluding files][analyzer-exclude-issue] via 10 | `analysis_options.yaml`. 11 | 12 | Add `tuneup` as a dev dependency to your project: 13 | 14 | ```yaml 15 | # pubspec.yaml 16 | dev_dependencies: 17 | tuneup: ^0.3.6 18 | ``` 19 | 20 | Use it in your dart_dev config: 21 | 22 | ```dart 23 | // tool/dart_dev/config.dart 24 | import 'package:dart_dev/dart_dev.dart'; 25 | 26 | final config = { 27 | 'analyze': TuneupCheckTool() 28 | }; 29 | ``` 30 | 31 | ## Default behavior 32 | 33 | By default this tool will run `dart run tuneup check` which will analyze all dart 34 | files in the current project. 35 | 36 | ## Ignoring info outputs 37 | 38 | By default, `dart run tuneup check` will include "info"-level analysis messages 39 | in its output and fail if there are any. You can tell tuneup to ignore these: 40 | 41 | ```dart 42 | // tool/dart_dev/config.dart 43 | import 'package:dart_dev/dart_dev.dart'; 44 | 45 | final config = { 46 | 'analyze': TuneupCheckTool() 47 | ..ignoreInfos = true, 48 | }; 49 | ``` 50 | 51 | ## Excluding files from analysis 52 | 53 | The `analysis_options.yaml` configuration file 54 | [supports excluding files][analysis-exclude]. 55 | 56 | ## Command-line options 57 | 58 | ```bash 59 | $ ddev help analyze 60 | ``` 61 | 62 | [analyzer-exclude-issue]: https://github.com/dart-lang/sdk/issues/25551 63 | [analysis-exclude]: https://dart.dev/guides/language/analysis-options#excluding-code-from-analysis 64 | 65 | --- 66 | --- 67 | 68 | 69 | 70 | - Tools 71 | - [`AnalyzeTool`][analyze-tool] 72 | - [`FormatTool`][format-tool] 73 | - [`TestTool`][test-tool] 74 | - [`TuneupCheckTool`][tuneup-check-tool] 75 | - [`WebdevServeTool`][webdev-serve-tool] 76 | - [Creating, Extending, and Composing Tools][tool-composition] 77 | - [v3 upgrade guide][v3-upgrade-guide] 78 | 79 | 80 | [analyze-tool]: /doc/tools/analyze-tool.md 81 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 82 | [dart-function-tool]: /doc/tools/dart-function-tool.md 83 | [format-tool]: /doc/tools/format-tool.md 84 | [process-tool]: /doc/tools/process-tool.md 85 | [test-tool]: /doc/tools/test-tool.md 86 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 87 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 88 | [tool-composition]: /doc/tool-composition.md 89 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 90 | -------------------------------------------------------------------------------- /doc/tools/webdev-serve-tool.md: -------------------------------------------------------------------------------- 1 | # `WebdevServeTool` 2 | 3 | Runs a local web development server for the current project using the `webdev` 4 | package. 5 | 6 | ## Usage 7 | 8 | ```dart 9 | // tool/dart_dev/config.dart 10 | import 'package:dart_dev/dart_dev.dart'; 11 | 12 | final config = { 13 | 'serve': WebdevServeTool() // configure as necessary 14 | }; 15 | ``` 16 | 17 | ## Default behavior 18 | 19 | By default this tool will run `dart pub global run webdev serve` which will build the 20 | `web/` directory using the Dart Dev Compiler and serve it on port 8080. 21 | 22 | ## Configuration 23 | 24 | ### Passing args to the webdev process 25 | 26 | ```dart 27 | // tool/dart_dev/config.dart 28 | import 'package:dart_dev/dart_dev.dart'; 29 | 30 | final config = { 31 | 'serve': WebdevServeTool() 32 | ..webdevArgs = ['--auto=refresh'] 33 | }; 34 | ``` 35 | 36 | ### Passing args to the underlying build_runner process 37 | 38 | ```dart 39 | // tool/dart_dev/config.dart 40 | import 'package:dart_dev/dart_dev.dart'; 41 | 42 | final config = { 43 | 'serve': WebdevServeTool() 44 | ..buildArgs = ['--delete-conflicting-outputs'] 45 | }; 46 | ``` 47 | 48 | ## Command-line options 49 | 50 | ```bash 51 | $ ddev help serve 52 | ``` 53 | 54 | --- 55 | --- 56 | 57 | 58 | 59 | - Tools 60 | - [`AnalyzeTool`][analyze-tool] 61 | - [`FormatTool`][format-tool] 62 | - [`TestTool`][test-tool] 63 | - [`TuneupCheckTool`][tuneup-check-tool] 64 | - [`WebdevServeTool`][webdev-serve-tool] 65 | - [Creating, Extending, and Composing Tools][tool-composition] 66 | - [v3 upgrade guide][v3-upgrade-guide] 67 | 68 | 69 | [analyze-tool]: /doc/tools/analyze-tool.md 70 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 71 | [dart-function-tool]: /doc/tools/dart-function-tool.md 72 | [format-tool]: /doc/tools/format-tool.md 73 | [process-tool]: /doc/tools/process-tool.md 74 | [test-tool]: /doc/tools/test-tool.md 75 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 76 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 77 | [tool-composition]: /doc/tool-composition.md 78 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 79 | -------------------------------------------------------------------------------- /doc/v3-upgrade-guide.md: -------------------------------------------------------------------------------- 1 | # Upgrading from v2 to v3 2 | 3 | Nothing fundamental has changed in terms of the goal of this package. However, 4 | v3 _is_ a breaking release and as a consumer, your project configuration file 5 | will need to be updated in order to consume it. 6 | 7 | The updated [readme] is a good place to start. It provides a refreshed overview 8 | of dart_dev and how it works. Once you've read that, this guide will hopefully 9 | help draw connections from the old to the new with some examples. 10 | 11 | ## Configuration File & Syntax 12 | 13 | With v2, the `package:dart_dev/dart_dev.dart` entrypoint exported a mutable 14 | `config` object with sub-objects for each configurable task. The `tool/dev.dart` 15 | file would feature a `main()` block that configured the `config` object as 16 | needed. 17 | 18 | ```dart test=false 19 | // tool/dev.dart -- v2 20 | import 'package:dart_dev/dart_dev.dart'; 21 | 22 | void main(args) async { 23 | config 24 | ..format.paths = ['lib/', 'test/'] 25 | ..test.unitTests = ['test/unit/']; 26 | await dev(args); 27 | } 28 | ``` 29 | 30 | In v3, dart_dev now expects a top-level `Map config` getter to 31 | exist in `tool/dart_dev/config.dart`. The keys in this map are the command names 32 | (i.e. a key of `format` means that it is runnable via `ddev format`), and the 33 | values are the implementations of the tool. 34 | 35 | ```dart 36 | // tool/dart_dev/config.dart -- v3 37 | import 'package:dart_dev/dart_dev.dart'; 38 | 39 | final config = { 40 | 'analyze': AnalyzeTool()..analyzerArgs = ['--fatal-hints'], 41 | 'format': FormatTool(), 42 | 'serve': WebdevServeTool()..webdevArgs = ['web:9000'], 43 | 'test': TestTool(), 44 | }; 45 | ``` 46 | 47 | This updated config pattern has a couple of important differences: 48 | 49 | - The available commands are completely configurable. With v2, we were pretty 50 | much locked into the commands defined in this package. There was some 51 | rudimentary support for "local tasks" defined in the `tool/` directory, but 52 | those were not then easily shared. In v3 you have complete control, which 53 | makes it much easier to extend or compose functionality. 54 | 55 | - There is no `main()` block, which makes the setup a bit simpler (you don't 56 | have to call `dev(args)`). Any runtime logic that previously lived in this 57 | `main()` block can be moved either to top-level variable declarations or 58 | functions that are then called by one or more `DevTool`s. 59 | 60 | ## v2 Tasks 61 | 62 | The core tasks supported in v2 are still available in v3, but some have been 63 | intentionally left behind. 64 | 65 | ### Analyze, Format, and Test 66 | 67 | These are the core developer tasks and they are still available by default via 68 | `ddev analyze`, `ddev format`, and `ddev test`. If you are using the shared 69 | `coreConfig`, they are included there, as well. If you would like to configure 70 | these tools further, you can do so: 71 | 72 | ```dart 73 | // tool/dart_dev/config.dart 74 | import 'package:dart_dev/dart_dev.dart'; 75 | 76 | final config = { 77 | // Construct the tool instances and configure their fields as desired. 78 | 'analyze': AnalyzeTool(), 79 | 'format': FormatTool(), 80 | 'test': TestTool(), 81 | }; 82 | ``` 83 | 84 | ### `ddev copy-license` 85 | 86 | This task is not specific to Dart development and would be more useful as a 87 | separate, general-purpose tool. In fact, some already exist. At this time, there 88 | are no plans to add this functionality back to v3. 89 | 90 | ### `ddev coverage` 91 | 92 | Coverage collection facilitated by the `test` package is [planned and partially 93 | in progress][coverage]. As soon as the `--coverage` option is available, we have 94 | plans to complement the coverage collection with automatic coverage formatting 95 | and HTML report generation. 96 | 97 | Instead of this being a separate task, it will likely just be a flag: 98 | 99 | ```bash 100 | $ ddev test --coverage 101 | ``` 102 | 103 | ### `ddev dart1-only / dart2-only` 104 | 105 | These tasks were provided as a convenience for the migration from dart 1 to dart 106 | 2 and are no longer provided as v3 only supports dart 2. 107 | 108 | ### `ddev gen-test-runner` 109 | 110 | Generating aggregated test suites/runners is no longer supported. The original 111 | impetus behind these generated runners was to speed up full test runs by only 112 | having to load one (or a few) test suites, but the startup time for individual 113 | test files has improved since then (probably ~3 years ago) and this is no longer 114 | as much of a concern. Additionally, treating every test file as its own suite 115 | has the advantage of being able to run each test file on its own (IDEs now 116 | support doing this directly from the test file) and allows us to leverage 117 | features like build_runner's `--build-filter` for large projects to speed up 118 | rebuild time when iterating on a subset of tests. 119 | 120 | There was one particularly useful feature of generated test runners, which was 121 | being able to share an HTML file – you wouldn't have to litter your `test/` 122 | directory with an HTML file for every test that requires it (common with 123 | over_react consumers). The [`test_html_builder` package][test-html-builder] was 124 | created to serve this specific use case. 125 | 126 | ### `ddev init` 127 | 128 | In v3, the boilerplate for `tool/dart_dev/config.dart` is pretty minimal and if 129 | omitted altogether the shared `coreConfig` will be used by default. For projects 130 | that do want to configure, it is easy to copy & paste from other projects or the 131 | readme. 132 | 133 | ### `ddev task-runner` 134 | 135 | This is not specific to dart development and has been removed from dart_dev. We 136 | recommend using the [GNU Parallel tool][parallel] as a replacement. It can be 137 | installed with pretty much any package manager (e.g. `brew install parallel`) 138 | and is much more fully-featured. 139 | 140 | ### `ddev sass` 141 | 142 | There now exists a [`sass_builder` package][sass-builder] that can handle automatically 143 | compiling your SASS files via the dart 2 build system. 144 | 145 | > Workiva developers: for the time being, a `ddev sass` target is still 146 | > available via the `dart_dev_workiva` package and should be used. Once we 147 | > address the root cause of slow compilation of our shared SASS libraries via 148 | > `sass_builder`, we will switch. 149 | 150 | [coverage]: https://github.com/dart-lang/test/issues/36 151 | [parallel]: https://www.gnu.org/software/parallel/ 152 | [readme]: /README.md 153 | [sass-builder]: https://pub.dev/packages/sass_builder 154 | [test-html-builder]: https://pub.dev/packages/test_html_builder 155 | 156 | --- 157 | --- 158 | 159 | 160 | 161 | - Tools 162 | - [`AnalyzeTool`][analyze-tool] 163 | - [`FormatTool`][format-tool] 164 | - [`TestTool`][test-tool] 165 | - [`TuneupCheckTool`][tuneup-check-tool] 166 | - [`WebdevServeTool`][webdev-serve-tool] 167 | - [Creating, Extending, and Composing Tools][tool-composition] 168 | - [v3 upgrade guide][v3-upgrade-guide] 169 | 170 | 171 | [analyze-tool]: /doc/tools/analyze-tool.md 172 | [tuneup-check-tool]: /doc/tools/tuneup-check-tool.md 173 | [dart-function-tool]: /doc/tools/dart-function-tool.md 174 | [format-tool]: /doc/tools/format-tool.md 175 | [process-tool]: /doc/tools/process-tool.md 176 | [test-tool]: /doc/tools/test-tool.md 177 | [webdev-build-tool]: /doc/tools/webdev-build-tool.md 178 | [webdev-serve-tool]: /doc/tools/webdev-serve-tool.md 179 | [tool-composition]: /doc/tool-composition.md 180 | [v3-upgrade-guide]: /doc/v3-upgrade-guide.md 181 | -------------------------------------------------------------------------------- /images/file_watcher_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/images/file_watcher_config.png -------------------------------------------------------------------------------- /images/file_watcher_pane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/images/file_watcher_pane.png -------------------------------------------------------------------------------- /lib/dart_dev.dart: -------------------------------------------------------------------------------- 1 | export 'src/core_config.dart' show coreConfig; 2 | export 'src/dart_dev_tool.dart' 3 | show DevTool, DevToolCommand, DevToolExecutionContext; 4 | export 'src/tools/analyze_tool.dart' show AnalyzeTool; 5 | export 'src/tools/compound_tool.dart' 6 | show ArgMapper, CompoundTool, CompoundToolMixin, takeAllArgs; 7 | export 'src/tools/format_tool.dart' 8 | show FormatMode, Formatter, FormatterInputs, FormatTool; 9 | export 'src/tools/process_tool.dart' show BackgroundProcessTool, ProcessTool; 10 | export 'src/tools/test_tool.dart' show TestTool; 11 | export 'src/tools/tuneup_check_tool.dart' show TuneupCheckTool; 12 | export 'src/tools/webdev_serve_tool.dart' show WebdevServeTool; 13 | -------------------------------------------------------------------------------- /lib/events.dart: -------------------------------------------------------------------------------- 1 | export 'src/events.dart' show CommandResult, onCommandComplete; 2 | -------------------------------------------------------------------------------- /lib/src/core_config.dart: -------------------------------------------------------------------------------- 1 | /// A `tool/dart_dev/config.dart` base configuration with the core Dart 2 | /// developer tasks. Intended to help standardize dart_dev configuration and 3 | /// command-line usage across Dart projects. 4 | library dart_dev.src.core_config; 5 | 6 | import 'package:dart_dev/dart_dev.dart'; 7 | 8 | Map get coreConfig => { 9 | 'analyze': AnalyzeTool(), 10 | 'format': FormatTool(), 11 | 'test': TestTool(), 12 | }; 13 | -------------------------------------------------------------------------------- /lib/src/dart_dev_runner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:args/command_runner.dart'; 5 | import 'package:dart_dev/dart_dev.dart'; 6 | 7 | import 'dart_dev_tool.dart'; 8 | import 'events.dart' as events; 9 | import 'tools/clean_tool.dart'; 10 | import 'utils/version.dart'; 11 | 12 | class DartDevRunner extends CommandRunner { 13 | DartDevRunner(Map commands) 14 | : super('dart_dev', 'Dart tool runner.') { 15 | // For backwards-compatibility, only add the `clean` command if it doesn't 16 | // conflict with any configured command. 17 | if (!commands.containsKey('clean')) { 18 | // Construct a new commands map here, to work around a runtime typecheck 19 | // failure: 20 | // `type 'CleanTool' is not a subtype of type 'FormatTool' of 'value'` 21 | // As seen in this CI run: 22 | // https://github.com/Workiva/dart_dev/actions/runs/8161855516/job/22311519665?pr=426#step:8:295 23 | commands = {...commands, 'clean': CleanTool()}; 24 | } 25 | 26 | commands.forEach((name, builder) { 27 | final command = builder.toCommand(name); 28 | if (command.name != name) { 29 | throw CommandNameMismatch(command.name, name); 30 | } 31 | addCommand(command); 32 | }); 33 | 34 | argParser 35 | ..addFlag('verbose', 36 | abbr: 'v', negatable: false, help: 'Enables verbose logging.') 37 | ..addFlag('version', 38 | negatable: false, help: 'Prints the dart_dev version.'); 39 | } 40 | 41 | @override 42 | ArgResults parse(Iterable args) { 43 | // TODO: get this completion working with bash/zsh 44 | // try { 45 | // return completion.tryArgsCompletion(args, argParser); 46 | // } catch (_) { 47 | // return super.parse(args); 48 | // } 49 | return super.parse(args); 50 | } 51 | 52 | @override 53 | Future run(Iterable args) async { 54 | final argResults = parse(args); 55 | if (argResults['version'] ?? false) { 56 | print(dartDevVersion); 57 | return 0; 58 | } 59 | final stopwatch = Stopwatch()..start(); 60 | final exitCode = (await super.run(args)) ?? 0; 61 | stopwatch.stop(); 62 | await events.commandComplete( 63 | events.CommandResult(args.toList(), exitCode, stopwatch.elapsed)); 64 | return exitCode; 65 | } 66 | } 67 | 68 | class CommandNameMismatch implements Exception { 69 | final String actual; 70 | final String expected; 71 | CommandNameMismatch(this.actual, this.expected); 72 | 73 | @override 74 | String toString() => 'CommandNameMismatch: ' 75 | 'Expected a "$expected" command but got one named "$actual".'; 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/dart_dev_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:args/command_runner.dart'; 6 | import 'package:dart_dev/dart_dev.dart'; 7 | 8 | import 'tools/function_tool.dart'; 9 | import 'utils/verbose_enabled.dart'; 10 | 11 | abstract class DevTool { 12 | DevTool(); 13 | 14 | factory DevTool.fromFunction( 15 | FutureOr Function(DevToolExecutionContext context) function, 16 | {ArgParser? argParser}) => 17 | FunctionTool(function, argParser: argParser); 18 | 19 | factory DevTool.fromProcess(String executable, List args, 20 | {ProcessStartMode? mode, String? workingDirectory}) => 21 | ProcessTool(executable, args, 22 | mode: mode, workingDirectory: workingDirectory); 23 | 24 | /// The argument parser for this tool, if needed. 25 | /// 26 | /// When this tool is run from the command-line, this will be used to parse 27 | /// the arguments. The results will be available via the 28 | /// [DevToolExecutionContext] provided when calling [run]. 29 | ArgParser? get argParser => null; 30 | 31 | /// This tool's description (which is included in the help/usage output) can 32 | /// be overridden by setting this field to a non-null value. 33 | String? description; 34 | 35 | /// This field determines whether or not this tool is hidden from the 36 | /// help/usage output when running as a part of a command-line app. 37 | /// 38 | /// By default, tools are not hidden. 39 | bool hidden = false; 40 | 41 | /// Runs this tool and returns (either synchronously or asynchronously) an 42 | /// int which will be treated as the exit code (i.e. non-zero means failure). 43 | /// 44 | /// [context] is optional. If calling this directly from a dart script, you 45 | /// will most likely want to omit this. [DevTool]s that are converted to a 46 | /// [DevToolCommand] via [toCommand] for use in a command-line application 47 | /// will provide a fully-populated [DevToolExecutionContext] here. 48 | /// 49 | /// This is the one API member that subclasses need to implement. 50 | FutureOr run([DevToolExecutionContext? context]); 51 | 52 | /// Converts this tool to a [Command] that can be added directly to a 53 | /// [CommandRunner], therefore making it executable from the command-line. 54 | /// 55 | /// The default implementation of this method returns a [Command] that calls 56 | /// [run] when it is executed. 57 | /// 58 | /// This method can be overridden by subclasses to return a custom 59 | /// implementation/extension of [DevToolCommand]. 60 | /// class CustomTool extends DevTool { 61 | /// @override 62 | /// Command toCommand(String name) => CustomCommand(name, this); 63 | /// } 64 | /// 65 | /// class CustomCommand extends DevToolCommand { 66 | /// CustomCommand(String name, DevTool devTool) : super(name, devTool); 67 | /// 68 | /// @override 69 | /// String get usageFooter => 'Custom usage footer...'; 70 | /// } 71 | Command toCommand(String name) => DevToolCommand(name, this); 72 | } 73 | 74 | /// A representation of the command-line execution context in which a [DevTool] 75 | /// is being run. 76 | /// 77 | /// An instance of this class should be created by [DevToolCommand] when calling 78 | /// [DevTool.run] so that the tool can utilize the parsed arg results, whether 79 | /// or not global verbose mode is enabled, and the [usageException] utility 80 | /// function from [Command]. 81 | class DevToolExecutionContext { 82 | DevToolExecutionContext( 83 | {this.argResults, 84 | this.commandName, 85 | void Function(String message)? usageException, 86 | this.verbose = false}) 87 | : _usageException = usageException; 88 | 89 | final void Function(String message)? _usageException; 90 | 91 | /// The results from parsing the arguments passed to a [Command] if this tool 92 | /// was executed via a command-line app. 93 | /// 94 | /// This may be null. 95 | final ArgResults? argResults; 96 | 97 | /// The name of the [Command] that executed this tool if it was executed via a 98 | /// command-line app. 99 | /// 100 | /// This may be null. 101 | final String? commandName; 102 | 103 | /// Whether the `-v|--verbose` flag was enabled when running the [Command] 104 | /// that executed this tool if it was executed via a command-line app. 105 | /// 106 | /// This will not be null; it defaults to `false`. 107 | final bool verbose; 108 | 109 | /// Return a copy of this instance with optional updates; any field that does 110 | /// not have an updated value will remain the same. 111 | DevToolExecutionContext update({ 112 | ArgResults? argResults, 113 | String? commandName, 114 | void Function(String message)? usageException, 115 | bool? verbose, 116 | }) => 117 | DevToolExecutionContext( 118 | argResults: argResults ?? this.argResults, 119 | commandName: commandName ?? this.commandName, 120 | usageException: usageException ?? this.usageException, 121 | verbose: verbose ?? this.verbose, 122 | ); 123 | 124 | /// Calling this will throw a [UsageException] with [message] that should be 125 | /// caught by [CommandRunner] and used to set the exit code accordingly and 126 | /// print out usage information. 127 | void usageException(String message) { 128 | if (_usageException != null) { 129 | _usageException!(message); 130 | } 131 | throw UsageException(message, ''); 132 | } 133 | } 134 | 135 | class DevToolCommand extends Command { 136 | DevToolCommand(this.name, this.devTool); 137 | 138 | @override 139 | ArgParser get argParser => devTool.argParser ?? super.argParser; 140 | 141 | @override 142 | String get description => devTool.description ?? ''; 143 | 144 | final DevTool devTool; 145 | 146 | @override 147 | bool get hidden => devTool.hidden; 148 | 149 | @override 150 | final String name; 151 | 152 | @override 153 | FutureOr? run() async => 154 | (await devTool.run( 155 | DevToolExecutionContext( 156 | argResults: argResults, 157 | commandName: name, 158 | usageException: usageException, 159 | verbose: verboseEnabled(this), 160 | ), 161 | )) ?? 162 | 0; 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/events.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | Future commandComplete(CommandResult result) async { 4 | await Future.wait(_commandCompleteListeners 5 | .map((listener) => Future.value(listener(result)))); 6 | } 7 | 8 | void onCommandComplete(FutureOr Function(CommandResult result) callback) { 9 | _commandCompleteListeners.add(callback); 10 | } 11 | 12 | final _commandCompleteListeners = 13 | Function(CommandResult result)>[]; 14 | 15 | class CommandResult { 16 | CommandResult(this.args, this.exitCode, this.duration); 17 | final List args; 18 | final Duration duration; 19 | final int exitCode; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/tools/analyze_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:glob/glob.dart'; 6 | import 'package:glob/list_local_fs.dart'; 7 | import 'package:logging/logging.dart'; 8 | 9 | import '../dart_dev_tool.dart'; 10 | import '../utils/arg_results_utils.dart'; 11 | import '../utils/assert_no_positional_args_nor_args_after_separator.dart'; 12 | import '../utils/dart_semver_version.dart'; 13 | import '../utils/executables.dart' as exe; 14 | import '../utils/logging.dart'; 15 | import '../utils/process_declaration.dart'; 16 | import '../utils/run_process_and_ensure_exit.dart'; 17 | 18 | final _log = Logger('Analyze'); 19 | 20 | /// A dart_dev tool that runs the `dartanalyzer` or `dart analyze` on the current project. 21 | /// If the `useDartAnalyze` flag is not specified it will default to `dartanalyzer`. 22 | /// 23 | /// To use this tool in your project, include it in the dart_dev config in 24 | /// `tool/dart_dev/config.dart`: 25 | /// import 'package:dart_dev/dart_dev.dart'; 26 | /// 27 | /// final config = { 28 | /// 'analyze': AnalyzeTool() ..useDartAnalyze = true, 29 | /// }; 30 | /// 31 | /// This will make it available via the `dart_dev` command-line app like so: 32 | /// dart run dart_dev analyze 33 | /// 34 | /// This tool can be configured by modifying any of its fields: 35 | /// // tool/dart_dev/config.dart 36 | /// import 'package:dart_dev/dart_dev.dart'; 37 | /// 38 | /// final config = { 39 | /// 'analyze': AnalyzeTool() 40 | /// ..analyzerArgs = ['--fatal-infos'] 41 | /// ..include = [Glob('.'), Glob('other/**.dart')], 42 | /// ..useDartAnalyze = true 43 | /// }; 44 | /// 45 | /// It is also possible to run this tool directly in a dart script: 46 | /// AnalyzeTool().run(); 47 | class AnalyzeTool extends DevTool { 48 | /// The args to pass to the `dartanalyzer` or `dart analyze` process run by this tool. 49 | /// 50 | /// Run `dartanalyzer -h -v` or `dart analyze -h -v` to see all available args. 51 | List? analyzerArgs; 52 | 53 | /// The globs to include as entry points to run static analysis on. 54 | /// 55 | /// The default is `.` (e.g. `dartanalyzer .`) which runs analysis on all Dart 56 | /// files in the current working directory. 57 | List? include; 58 | 59 | /// The default tool for analysis will be `dartanalyzer` unless opted in here 60 | /// to utilize `dart analyze`. 61 | bool? useDartAnalyze; 62 | 63 | // --------------------------------------------------------------------------- 64 | // DevTool Overrides 65 | // --------------------------------------------------------------------------- 66 | 67 | @override 68 | final ArgParser argParser = ArgParser() 69 | ..addOption('analyzer-args', 70 | help: 'Args to pass to the "dartanalyzer" or "dart analyze" process.\n' 71 | 'Run "dartanalyzer -h -v" or `dart analyze -h -v" to see all available options.'); 72 | 73 | @override 74 | String? description = 'Run static analysis on dart files in this package.'; 75 | 76 | @override 77 | FutureOr run([DevToolExecutionContext? context]) { 78 | return runProcessAndEnsureExit( 79 | buildProcess( 80 | context ?? DevToolExecutionContext(), 81 | configuredAnalyzerArgs: analyzerArgs, 82 | include: include, 83 | useDartAnalyze: 84 | !dartVersionHasDartanalyzer ? true : useDartAnalyze ?? false, 85 | ), 86 | log: _log); 87 | } 88 | } 89 | 90 | /// Returns a combined list of args for the `dartanalyzer` 91 | /// or `dart analyze` process. 92 | /// 93 | /// If [configuredAnalyzerArgs] is non-null, they will be included first. 94 | /// 95 | /// If [argResults] is non-null and the `--analyzer-args` option is non-null, 96 | /// they will be included second. 97 | /// 98 | /// If [verbose] is true and the verbose flag (`-v`) is not already included, it 99 | /// will be added. 100 | Iterable buildArgs( 101 | {ArgResults? argResults, 102 | List? configuredAnalyzerArgs, 103 | bool useDartAnalyze = false, 104 | bool verbose = false}) { 105 | final args = [ 106 | // Combine all args that should be passed through to the analyzer in 107 | // this order: 108 | // 1. The analyze command if using dart analyze 109 | if (useDartAnalyze) 'analyze', 110 | // 2. Statically configured args from [AnalyzeTool.analyzerArgs] 111 | ...?configuredAnalyzerArgs, 112 | // 3. Args passed to --analyzer-args 113 | ...?splitSingleOptionValue(argResults, 'analyzer-args'), 114 | ]; 115 | if (verbose && !args.contains('-v') && !args.contains('--verbose')) { 116 | args.add('-v'); 117 | } 118 | return args; 119 | } 120 | 121 | /// Returns the entrypoint paths obtained by expanding the given [include] globs 122 | /// and returning a default of `['.']` if none were found. 123 | /// 124 | /// By default these globs are assumed to be relative to the current working 125 | /// directory, but that can be overridden via [root] for testing purposes. 126 | Iterable buildEntrypoints({List? include, String? root}) { 127 | include ??= []; 128 | final entrypoints = { 129 | for (final glob in include) 130 | ...glob 131 | .listSync(root: root) 132 | .where((entity) => entity is File || entity is Directory) 133 | .map((entity) => entity.path), 134 | }; 135 | if (entrypoints.isEmpty) { 136 | entrypoints.add('.'); 137 | } 138 | return entrypoints; 139 | } 140 | 141 | /// Returns a declarative representation of an analyzer process to run based on 142 | /// the given parameters. 143 | /// 144 | /// These parameters will be populated from [AnalyzeTool] when it is executed 145 | /// (either directly or via a command-line app). 146 | /// 147 | /// [context] is the execution context that would be provided by [AnalyzeTool] 148 | /// when converted to a [DevToolCommand]. For tests, this can be manually 149 | /// created to imitate the various CLI inputs. 150 | /// 151 | /// [configuredAnalyzerArgs] will be populated from [AnalyzeTool.analyzerArgs]. 152 | /// 153 | /// [include] will be populated from [AnalyzeTool.include]. 154 | /// 155 | /// If non-null, [path] will override the current working directory for any 156 | /// operations that require it. This is intended for use by tests. 157 | /// 158 | /// If true, [useDartAnalyze] will utilize `dart analyze` for analysis. 159 | /// If null, it will default to utilze `dartanalyzer`. 160 | /// 161 | /// The [AnalyzeTool] can be tested almost completely via this function by 162 | /// enumerating all of the possible parameter variations and making assertions 163 | /// on the declarative output. 164 | ProcessDeclaration buildProcess( 165 | DevToolExecutionContext context, { 166 | List? configuredAnalyzerArgs, 167 | List? include, 168 | String? path, 169 | bool useDartAnalyze = false, 170 | }) { 171 | final argResults = context.argResults; 172 | if (argResults != null) { 173 | final analyzerUsed = useDartAnalyze ? 'dart analyze' : 'dartanalyzer'; 174 | assertNoPositionalArgsNorArgsAfterSeparator( 175 | argResults, context.usageException, 176 | commandName: context.commandName, 177 | usageFooter: 178 | 'Arguments can be passed to the "$analyzerUsed" process via ' 179 | 'the --analyzer-args option.'); 180 | } 181 | var executable = useDartAnalyze ? exe.dart : exe.dartanalyzer; 182 | final args = buildArgs( 183 | argResults: context.argResults, 184 | configuredAnalyzerArgs: configuredAnalyzerArgs, 185 | verbose: context.verbose, 186 | useDartAnalyze: useDartAnalyze); 187 | final entrypoints = buildEntrypoints(include: include, root: path); 188 | logCommand(args, entrypoints, 189 | verbose: context.verbose, useDartAnalyzer: useDartAnalyze); 190 | return ProcessDeclaration(executable, [...args, ...entrypoints], 191 | mode: ProcessStartMode.inheritStdio); 192 | } 193 | 194 | /// Logs the `dartanalyzer` or `dart analyze` command that will be run by [AnalyzeTool] so that 195 | /// consumers can run it directly for debugging purposes. 196 | /// 197 | /// Unless [verbose] is true, the list of entrypoints will be abbreviated to 198 | /// avoid an unnecessarily long log. 199 | void logCommand( 200 | Iterable args, 201 | Iterable entrypoints, { 202 | bool useDartAnalyzer = false, 203 | bool verbose = false, 204 | }) { 205 | final exeAndArgs = 206 | '${useDartAnalyzer ? "dart" : "dartanalyzer"} ${args.join(' ')}'.trim(); 207 | 208 | if (entrypoints.length <= 5 || verbose) { 209 | logSubprocessHeader(_log, '$exeAndArgs ${entrypoints.join(' ')}'); 210 | } else { 211 | logSubprocessHeader(_log, '$exeAndArgs <${entrypoints.length} paths>'); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /lib/src/tools/clean_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_dev/src/utils/logging.dart'; 5 | import 'package:io/io.dart'; 6 | 7 | import '../dart_dev_tool.dart'; 8 | import '../utils/dart_dev_paths.dart' show DartDevPaths; 9 | 10 | class CleanTool extends DevTool { 11 | @override 12 | final String? description = 'Cleans up temporary files used by dart_dev.'; 13 | 14 | @override 15 | FutureOr run([DevToolExecutionContext? context]) { 16 | final cache = Directory(DartDevPaths().cache()); 17 | if (cache.existsSync()) { 18 | log.info('Deleting ${cache.path}'); 19 | cache.deleteSync(recursive: true); 20 | } else { 21 | log.info('Nothing to do: no ${cache.path} found'); 22 | } 23 | return ExitCode.success.code; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/tools/function_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:io/io.dart'; 5 | import 'package:logging/logging.dart'; 6 | 7 | import '../dart_dev_tool.dart'; 8 | import '../utils/assert_no_positional_args_nor_args_after_separator.dart'; 9 | 10 | /// A utility class designed to make it simple to create a [DevTool] from just a 11 | /// Dart function. 12 | /// 13 | /// Use [DevTool.fromFunction] to create [FunctionTool] instances. 14 | class FunctionTool extends DevTool { 15 | FunctionTool( 16 | FutureOr Function(DevToolExecutionContext context) function, 17 | {ArgParser? argParser}) 18 | : _argParser = argParser, 19 | _function = function; 20 | 21 | final FutureOr Function(DevToolExecutionContext context) _function; 22 | 23 | // --------------------------------------------------------------------------- 24 | // DevTool Overrides 25 | // --------------------------------------------------------------------------- 26 | 27 | @override 28 | ArgParser? get argParser => _argParser; 29 | final ArgParser? _argParser; 30 | 31 | @override 32 | FutureOr run([DevToolExecutionContext? context]) async { 33 | context ??= DevToolExecutionContext(); 34 | final argResults = context.argResults; 35 | if (argResults != null) { 36 | if (argParser == null) { 37 | assertNoPositionalArgsNorArgsAfterSeparator( 38 | argResults, context.usageException, 39 | commandName: context.commandName); 40 | } 41 | } 42 | final exitCode = await _function(context); 43 | if (exitCode != null) { 44 | return exitCode; 45 | } 46 | Logger('DartFunctionTool').warning( 47 | '${context.commandName != null ? 'The ${context.commandName}' : 'This'}' 48 | ' command did not return an exit code.'); 49 | return ExitCode.software.code; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/tools/over_react_format_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_dev/dart_dev.dart'; 5 | import 'package:dart_dev/utils.dart'; 6 | 7 | import '../utils/executables.dart' as exe; 8 | import 'format_tool.dart'; 9 | 10 | class OverReactFormatTool extends DevTool { 11 | /// Wrap lines longer than this. 12 | /// 13 | /// Default is 80. 14 | int? lineLength; 15 | 16 | /// Whether or not to organize import/export directives. 17 | /// 18 | /// Default is false. 19 | bool? organizeDirectives; 20 | 21 | @override 22 | String? description = 23 | 'Format dart files in this package with over_react_format.'; 24 | 25 | @override 26 | FutureOr run([DevToolExecutionContext? context]) async { 27 | context ??= DevToolExecutionContext(); 28 | Iterable paths = context.argResults?.rest ?? []; 29 | if (paths.isEmpty) { 30 | context.usageException.call( 31 | '"hackFastFormat" must specify targets to format.\n' 32 | 'hackFastFormat should only be used to format specific files. ' 33 | 'Running the command over an entire project may format files that ' 34 | 'would be excluded using the standard "format" command.'); 35 | } 36 | final args = [ 37 | 'run', 38 | 'over_react_format', 39 | if (lineLength != null) '--line-length=$lineLength', 40 | if (organizeDirectives == true) '--organize-directives', 41 | ]; 42 | final process = ProcessDeclaration(exe.dart, [...args, ...paths], 43 | mode: ProcessStartMode.inheritStdio); 44 | logCommand('dart', paths, args, verbose: context.verbose); 45 | return runProcessAndEnsureExit(process); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/tools/process_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:io/io.dart'; 5 | import 'package:logging/logging.dart'; 6 | 7 | import '../dart_dev_tool.dart'; 8 | import '../utils/assert_no_positional_args_nor_args_after_separator.dart'; 9 | import '../utils/ensure_process_exit.dart'; 10 | import '../utils/logging.dart'; 11 | import '../utils/process_declaration.dart'; 12 | import '../utils/start_process_and_ensure_exit.dart'; 13 | 14 | final _log = Logger('Process'); 15 | 16 | /// A utility class designed to make it simple to create a [DevTool] that runs a 17 | /// process, waits for it to complete, and forwards its exit code. 18 | /// 19 | /// To create a [ProcessTool], all that is needed is an executable and args: 20 | /// import 'package:dart_dev/dart_dev.dart'; 21 | /// 22 | /// final config = { 23 | /// 'github': ProcessTool( 24 | /// 'open', ['https://github.com/Workiva/dart_dev']), 25 | /// }; 26 | /// 27 | /// It is also possible to run this tool directly in a dart script: 28 | /// ProcessTool(exe, args).run(); 29 | class ProcessTool extends DevTool { 30 | ProcessTool(String executable, List args, 31 | {ProcessStartMode? mode, String? workingDirectory}) 32 | : _args = args, 33 | _executable = executable, 34 | _mode = mode, 35 | _workingDirectory = workingDirectory; 36 | 37 | final List _args; 38 | final String _executable; 39 | final ProcessStartMode? _mode; 40 | final String? _workingDirectory; 41 | 42 | Process? get process => _process; 43 | Process? _process; 44 | 45 | @override 46 | FutureOr run([DevToolExecutionContext? context]) async { 47 | context ??= DevToolExecutionContext(); 48 | final argResults = context.argResults; 49 | if (argResults != null) { 50 | assertNoPositionalArgsNorArgsAfterSeparator( 51 | argResults, context.usageException, 52 | commandName: context.commandName); 53 | } 54 | logSubprocessHeader(_log, '$_executable ${_args.join(' ')}'); 55 | _process = await startProcessAndEnsureExit( 56 | ProcessDeclaration(_executable, _args, 57 | mode: _mode, workingDirectory: _workingDirectory), 58 | log: _log); 59 | return _process!.exitCode; 60 | } 61 | } 62 | 63 | class BackgroundProcessTool { 64 | final List _args; 65 | final String _executable; 66 | final ProcessStartMode? _mode; 67 | final Duration? _delayAfterStart; 68 | final String? _workingDirectory; 69 | 70 | BackgroundProcessTool(String executable, List args, 71 | {ProcessStartMode? mode, 72 | Duration? delayAfterStart, 73 | String? workingDirectory}) 74 | : _args = args, 75 | _executable = executable, 76 | _mode = mode, 77 | _delayAfterStart = delayAfterStart, 78 | _workingDirectory = workingDirectory; 79 | 80 | Process? get process => _process; 81 | Process? _process; 82 | 83 | DevTool get starter => DevTool.fromFunction(_start); 84 | 85 | DevTool get stopper => DevTool.fromFunction(_stop); 86 | 87 | bool _processHasExited = false; 88 | 89 | Future _start(DevToolExecutionContext context) async { 90 | final argResults = context.argResults; 91 | if (argResults != null) { 92 | assertNoPositionalArgsNorArgsAfterSeparator( 93 | argResults, context.usageException, 94 | commandName: context.commandName); 95 | } 96 | logSubprocessHeader(_log, '$_executable ${_args.join(' ')}'); 97 | 98 | final mode = _mode ?? 99 | (context.verbose 100 | ? ProcessStartMode.inheritStdio 101 | : ProcessStartMode.normal); 102 | _process = await Process.start(_executable, _args, 103 | mode: mode, workingDirectory: _workingDirectory); 104 | ensureProcessExit(_process!); 105 | unawaited(_process!.exitCode.then((_) => _processHasExited = true)); 106 | 107 | if (_delayAfterStart != null) { 108 | await Future.delayed(_delayAfterStart!); 109 | } 110 | 111 | if (_processHasExited) { 112 | // If the background process exits immediately or before the start delay, 113 | // something is probably wrong, so return that exit code. 114 | return _process!.exitCode; 115 | } 116 | 117 | return ExitCode.success.code; 118 | } 119 | 120 | Future _stop(DevToolExecutionContext context) async { 121 | final argResults = context.argResults; 122 | if (argResults != null) { 123 | assertNoPositionalArgsNorArgsAfterSeparator( 124 | argResults, context.usageException, 125 | commandName: context.commandName); 126 | } 127 | _log.info('Stopping: $_executable ${_args.join(' ')}'); 128 | _process?.kill(); 129 | await _process?.exitCode; 130 | return ExitCode.success.code; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/src/tools/tuneup_check_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:io/ansi.dart'; 6 | import 'package:io/io.dart'; 7 | import 'package:logging/logging.dart'; 8 | 9 | import '../dart_dev_tool.dart'; 10 | import '../utils/arg_results_utils.dart'; 11 | import '../utils/assert_no_positional_args_nor_args_after_separator.dart'; 12 | import '../utils/executables.dart' as exe; 13 | import '../utils/logging.dart'; 14 | import '../utils/package_is_immediate_dependency.dart'; 15 | import '../utils/process_declaration.dart'; 16 | import '../utils/run_process_and_ensure_exit.dart'; 17 | 18 | final _log = Logger('TuneupCheck'); 19 | 20 | /// A dart_dev tool that runs the `tuneup` on the current project. 21 | /// 22 | /// To use this tool in your project, include it in the dart_dev config in 23 | /// `tool/dart_dev/config.dart`: 24 | /// import 'package:dart_dev/dart_dev.dart'; 25 | /// 26 | /// final config = { 27 | /// 'analyze': TuneupCheckTool(), 28 | /// }; 29 | /// 30 | /// This will make it available via the `dart_dev` command-line app like so: 31 | /// dart run dart_dev analyze 32 | /// 33 | /// This tool can be configured by modifying any of its fields: 34 | /// // tool/dart_dev/config.dart 35 | /// import 'package:dart_dev/dart_dev.dart'; 36 | /// 37 | /// final config = { 38 | /// 'analyze': TuneupCheckTool() 39 | /// ..ignoreInfos = true, 40 | /// }; 41 | /// 42 | /// It is also possible to run this tool directly in a dart script: 43 | /// TuneupCheckTool().run(); 44 | class TuneupCheckTool extends DevTool { 45 | /// Whether `--ignore-infos` should be passed to `tuneup check`. 46 | bool? ignoreInfos; 47 | 48 | // --------------------------------------------------------------------------- 49 | // DevTool Overrides 50 | // --------------------------------------------------------------------------- 51 | 52 | @override 53 | final ArgParser argParser = ArgParser() 54 | ..addFlag('ignore-infos', help: 'Ignore any info level issues.'); 55 | 56 | @override 57 | String? description = 'Run static analysis on dart files in this package ' 58 | 'using the tuneup tool.'; 59 | 60 | @override 61 | FutureOr run([DevToolExecutionContext? context]) async { 62 | final execution = buildExecution(context ?? DevToolExecutionContext(), 63 | configuredIgnoreInfos: ignoreInfos); 64 | return execution.exitCode ?? 65 | await runProcessAndEnsureExit(execution.process!, log: _log); 66 | } 67 | } 68 | 69 | /// A declarative representation of an execution of the [TuneupCheckTool]. 70 | /// 71 | /// This class allows the [TuneupCheckTool] to break its execution up into 72 | /// two steps: 73 | /// 1. Validation of config/inputs and creation of this class. 74 | /// 2. Execution of expensive or hard-to-test logic based on step 1. 75 | /// 76 | /// As a result, nearly all of the logic in [TuneupCheckTool] can be tested 77 | /// via the output of step 1 with very simple unit tests. 78 | class TuneupExecution { 79 | TuneupExecution.exitEarly(this.exitCode) : process = null; 80 | TuneupExecution.process(this.process) : exitCode = null; 81 | 82 | /// If non-null, the execution is already complete and the 83 | /// [TuneupCheckTool] should exit with this code. 84 | /// 85 | /// If null, there is more work to do. 86 | final int? exitCode; 87 | 88 | /// A declarative representation of the test process that should be run. 89 | /// 90 | /// This process' result should become the final result of the 91 | /// [TuneupCheckTool]. 92 | final ProcessDeclaration? process; 93 | } 94 | 95 | /// Returns a combined list of args for the `tuneup` process. 96 | /// 97 | /// If [verbose] is true and the verbose flag (`-v`) is not already included, it 98 | /// will be added. 99 | Iterable buildArgs({ 100 | ArgResults? argResults, 101 | bool? configuredIgnoreInfos, 102 | bool verbose = false, 103 | }) { 104 | var ignoreInfos = (configuredIgnoreInfos ?? false) || 105 | (flagValue(argResults, 'ignore-infos') ?? false); 106 | return [ 107 | 'run', 108 | 'tuneup', 109 | 'check', 110 | if (ignoreInfos) '--ignore-infos', 111 | if (verbose) '--verbose', 112 | ]; 113 | } 114 | 115 | /// Returns a declarative representation of an tuneup process to run based on 116 | /// the given parameters. 117 | /// 118 | /// These parameters will be populated from [TuneupCheckTool] when it is 119 | /// executed (either directly or via a command-line app). 120 | /// 121 | /// [context] is the execution context that would be provided by 122 | /// [TuneupCheckTool] when converted to a [DevToolCommand]. For tests, this 123 | /// can be manually created to imitate the various CLI inputs. 124 | /// 125 | /// If non-null, [path] will override the current working directory for any 126 | /// operations that require it. This is intended for use by tests. 127 | /// 128 | /// The [TuneupCheckTool] can be tested almost completely via this function 129 | /// by enumerating all of the possible parameter variations and making 130 | /// assertions on the declarative output. 131 | TuneupExecution buildExecution( 132 | DevToolExecutionContext context, { 133 | bool? configuredIgnoreInfos, 134 | String? path, 135 | }) { 136 | final argResults = context.argResults; 137 | if (argResults != null) { 138 | assertNoPositionalArgsNorArgsAfterSeparator( 139 | argResults, context.usageException, 140 | commandName: context.commandName); 141 | } 142 | 143 | if (!packageIsImmediateDependency('tuneup', path: path)) { 144 | _log.severe(red.wrap('Cannot run "tuneup check".\n')! + 145 | yellow.wrap( 146 | 'You must have a dependency on "tuneup" in pubspec.yaml.\n')!); 147 | return TuneupExecution.exitEarly(ExitCode.config.code); 148 | } 149 | 150 | final args = buildArgs( 151 | argResults: argResults, 152 | configuredIgnoreInfos: configuredIgnoreInfos, 153 | verbose: context.verbose, 154 | ).toList(); 155 | logSubprocessHeader(_log, 'dart ${args.join(' ')}'); 156 | return TuneupExecution.process( 157 | ProcessDeclaration(exe.dart, args, mode: ProcessStartMode.inheritStdio)); 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/tools/webdev_serve_tool.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:io/ansi.dart'; 6 | import 'package:io/io.dart'; 7 | import 'package:logging/logging.dart'; 8 | import 'package:pub_semver/pub_semver.dart'; 9 | 10 | import '../dart_dev_tool.dart'; 11 | import '../utils/arg_results_utils.dart'; 12 | import '../utils/assert_no_positional_args_nor_args_after_separator.dart'; 13 | import '../utils/dart_semver_version.dart'; 14 | import '../utils/executables.dart' as exe; 15 | import '../utils/global_package_is_active_and_compatible.dart'; 16 | import '../utils/logging.dart'; 17 | import '../utils/process_declaration.dart'; 18 | import '../utils/run_process_and_ensure_exit.dart'; 19 | 20 | final _log = Logger('WebdevServe'); 21 | 22 | /// A dart_dev tool that runs a local web development server for the current 23 | /// project using the `webdev` package. 24 | /// 25 | /// To use this tool in your project, include it in the dart_dev config in 26 | /// `tool/dart_dev/config.dart`: 27 | /// import 'package:dart_dev/dart_dev.dart'; 28 | /// 29 | /// final config = { 30 | /// 'serve': WebdevServeTool(), 31 | /// }; 32 | /// 33 | /// This will make it available via the `dart_dev` command-line app like so: 34 | /// dart run dart_dev serve 35 | /// 36 | /// This tool can be configured by modifying any of its fields: 37 | /// // tool/dart_dev/config.dart 38 | /// import 'package:dart_dev/dart_dev.dart'; 39 | /// 40 | /// final config = { 41 | /// 'serve': WebdevServeTool() 42 | /// ..buildArgs = ['--delete-conflicting-outputs'] 43 | /// ..webdevArgs = ['--debug', 'example:8080', 'web:8081'] 44 | /// }; 45 | /// 46 | /// It is also possible to run this tool directly in a dart script: 47 | /// WebdevServeTool().run(); 48 | class WebdevServeTool extends DevTool { 49 | /// The args to pass to the `build_runner` process via the `webdev serve` 50 | /// process that will be run by this tool. 51 | /// 52 | /// Run `dart run build_runner build -h` to see all available args. 53 | List? buildArgs; 54 | 55 | /// The args to pass to the `webdev serve` process that will be run by this 56 | /// tool. 57 | /// 58 | /// Run `dart pub global run webdev serve -h` to see all available args. 59 | List? webdevArgs; 60 | 61 | // --------------------------------------------------------------------------- 62 | // DevTool Overrides 63 | // --------------------------------------------------------------------------- 64 | 65 | @override 66 | final ArgParser argParser = ArgParser() 67 | ..addFlag('release', 68 | abbr: 'r', help: 'Build with release mode defaults for builders.') 69 | ..addSeparator('======== Other Options') 70 | ..addOption('webdev-args', 71 | help: 'Args to pass to the webdev serve process.\n' 72 | 'Run "dart pub global run webdev serve -h -v" to see all available ' 73 | 'options.') 74 | ..addOption('build-args', 75 | help: 'Args to pass to the build runner process.\n' 76 | 'Run "dart run build_runner build -h -v" to see all available ' 77 | 'options.'); 78 | 79 | @override 80 | String? description = 'Run a local web development server and a file system ' 81 | 'watcher that rebuilds on changes.'; 82 | 83 | @override 84 | FutureOr run([DevToolExecutionContext? context]) async { 85 | context ??= DevToolExecutionContext(); 86 | final execution = buildExecution(context, 87 | configuredBuildArgs: buildArgs, configuredWebdevArgs: webdevArgs); 88 | return execution.exitCode ?? 89 | await runProcessAndEnsureExit(execution.process!, log: _log); 90 | } 91 | } 92 | 93 | /// A declarative representation of an execution of the [WebdevServeTool]. 94 | /// 95 | /// This class allows the [WebdevServeTool] to break its execution up into two 96 | /// steps: 97 | /// 1. Validation of config/inputs and creation of this class. 98 | /// 2. Execution of expensive or hard-to-test logic based on step 1. 99 | /// 100 | /// As a result, nearly all of the logic in [WebdevServeTool] can be tested via 101 | /// the output of step 1 with very simple unit tests. 102 | class WebdevServeExecution { 103 | WebdevServeExecution.exitEarly(this.exitCode) : process = null; 104 | WebdevServeExecution.process(this.process) : exitCode = null; 105 | 106 | /// If non-null, the execution is already complete and the [WebdevServeTool] 107 | /// should exit with this code. 108 | /// 109 | /// If null, there is more work to do. 110 | final int? exitCode; 111 | 112 | /// A declarative representation of the webdev process that should be run. 113 | /// 114 | /// This process' result should become the final result of the 115 | /// [WebdevServeTool]. 116 | final ProcessDeclaration? process; 117 | } 118 | 119 | /// Builds and returns the full list of args for the webdev serve process that 120 | /// [WebdevServeTool] will start. 121 | /// 122 | /// Since the `webdev` tool wraps a `build_runner` process, the returned list of 123 | /// args will be two portions with an arg separator between them, e.g.: 124 | /// dart pub global run webdev serve -- 125 | /// 126 | /// When building the webdev args portion of the list, the 127 | /// [configuredWebdevArgs] will be included first (if non-null) followed by the 128 | /// value of the `--webdev-args` option if it and [argResults] are non-null. 129 | /// 130 | /// When building the build args portion of the list, the [configuredBuildArgs] 131 | /// will be included first (if non-null), followed by the value of the 132 | /// `--build-args` option if it and [argResults] are non-null 133 | /// 134 | /// If [verbose] is true, both the webdev args and the build args portions of 135 | /// the returned list will include the `-v` verbose flag. 136 | List buildArgs( 137 | {ArgResults? argResults, 138 | List? configuredBuildArgs, 139 | List? configuredWebdevArgs, 140 | bool verbose = false}) { 141 | final webdevArgs = [ 142 | // Combine all args that should be passed through to the webdev serve 143 | // process in this order: 144 | // 1. Statically configured args from [WebdevServeTool.webdevArgs] 145 | ...?configuredWebdevArgs, 146 | // 2. The -r|--release flag 147 | if (argResults != null && argResults['release']) '--release', 148 | // 3. Args passed to --webdev-args 149 | ...?splitSingleOptionValue(argResults, 'webdev-args'), 150 | ]; 151 | final buildArgs = [ 152 | // Combine all args that should be passed through to the build_runner 153 | // process in this order: 154 | // 1. Statically configured args from [WebdevServeTool.buildArgs] 155 | ...?configuredBuildArgs, 156 | // 2. Args passed to --build-args 157 | ...?splitSingleOptionValue(argResults, 'build-args'), 158 | ]; 159 | 160 | if (verbose) { 161 | if (!buildArgs.contains('-v') && !buildArgs.contains('--verbose')) { 162 | buildArgs.add('-v'); 163 | } 164 | if (!webdevArgs.contains('-v') && !webdevArgs.contains('--verbose')) { 165 | webdevArgs.add('-v'); 166 | } 167 | } 168 | 169 | return [ 170 | 'pub', 171 | 'global', 172 | 'run', 173 | 'webdev', 174 | 'serve', 175 | ...webdevArgs, 176 | if (buildArgs.isNotEmpty) '--', 177 | ...buildArgs, 178 | ]; 179 | } 180 | 181 | /// Returns a declarative representation of a webdev process to run based on the 182 | /// given parameters. 183 | /// 184 | /// These parameters will be populated from [WebdevServeTool] when it is 185 | /// executed (either directly or via a command-line app). 186 | /// 187 | /// [context] is the execution context that would be provided by 188 | /// [WebdevServeTool] when converted to a [DevToolCommand]. For tests, this can 189 | /// be manually created to imitate the various CLI inputs. 190 | /// 191 | /// [configuredWebdevArgs] will be populated from [WebdevServeTool.webdevArgs]. 192 | /// 193 | /// [configuredBuildArgs] will be populated from [WebdevServeTool.buildArgs]. 194 | /// 195 | /// If non-null, [path] will override the current working directory for any 196 | /// operations that require it. This is intended for use by tests. 197 | /// 198 | /// The [WebdevServeTool] can be tested almost completely via this function by 199 | /// enumerating all of the possible parameter variations and making assertions 200 | /// on the declarative output. 201 | WebdevServeExecution buildExecution( 202 | DevToolExecutionContext context, { 203 | List? configuredBuildArgs, 204 | List? configuredWebdevArgs, 205 | Map? environment, 206 | }) { 207 | final argResults = context.argResults; 208 | if (argResults != null) { 209 | assertNoPositionalArgsNorArgsAfterSeparator( 210 | argResults, context.usageException, 211 | commandName: context.commandName, 212 | usageFooter: 'Arguments can be passed to the webdev process via the ' 213 | '--webdev-args option.\n' 214 | 'Arguments can be passed to the build process via the --build-args ' 215 | 'option.'); 216 | } 217 | 218 | final webdevVersion = dartSemverVersion.major == 2 ? '^2.0.0' : '^3.0.0'; 219 | 220 | if (!globalPackageIsActiveAndCompatible( 221 | 'webdev', VersionConstraint.parse(webdevVersion), 222 | environment: environment)) { 223 | _log.severe(red.wrap( 224 | '${styleBold.wrap('webdev serve')} could not run for this project.\n')! + 225 | yellow.wrap('You must have `webdev` globally activated:\n' 226 | ' dart pub global activate webdev ${webdevVersion}')!); 227 | return WebdevServeExecution.exitEarly(ExitCode.config.code); 228 | } 229 | 230 | final args = buildArgs( 231 | argResults: argResults, 232 | configuredBuildArgs: configuredBuildArgs, 233 | configuredWebdevArgs: configuredWebdevArgs, 234 | verbose: context.verbose); 235 | logSubprocessHeader(_log, 'dart ${args.join(' ')}'.trim()); 236 | return WebdevServeExecution.process( 237 | ProcessDeclaration(exe.dart, args, mode: ProcessStartMode.inheritStdio)); 238 | } 239 | -------------------------------------------------------------------------------- /lib/src/utils/arg_results_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | bool? flagValue(ArgResults? argResults, String name) { 4 | if (argResults == null || argResults[name] == null) { 5 | return null; 6 | } 7 | if (argResults[name] is! bool) { 8 | throw ArgumentError('Option "$name" is not a flag.'); 9 | } 10 | return argResults[name]; 11 | } 12 | 13 | Iterable? multiOptionValue(ArgResults? argResults, String name) { 14 | if (argResults == null || argResults[name] == null) { 15 | return null; 16 | } 17 | if (argResults[name] is! Iterable) { 18 | throw ArgumentError('Option "$name" is not a multi-option.'); 19 | } 20 | return List.from(argResults[name]); 21 | } 22 | 23 | String? singleOptionValue(ArgResults? argResults, String name) { 24 | if (argResults == null || argResults[name] == null) { 25 | return null; 26 | } 27 | if (argResults[name] is! String) { 28 | throw ArgumentError('Option "$name" is not a single option.'); 29 | } 30 | return argResults[name]; 31 | } 32 | 33 | Iterable? splitSingleOptionValue(ArgResults? argResults, String name) => 34 | singleOptionValue(argResults, name)?.split(' '); 35 | -------------------------------------------------------------------------------- /lib/src/utils/assert_dir_is_dart_package.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:path/path.dart' as p; 4 | 5 | void assertDirIsDartPackage({String? path}) { 6 | path ??= p.current; 7 | final pubspec = File(p.join(path, 'pubspec.yaml')); 8 | if (!pubspec.existsSync()) { 9 | throw DirectoryIsNotPubPackage(path); 10 | } 11 | } 12 | 13 | class DirectoryIsNotPubPackage implements Exception { 14 | final String path; 15 | 16 | DirectoryIsNotPubPackage(this.path); 17 | 18 | @override 19 | String toString() => 'Could not find a file named "pubspec.yaml" in "$path".'; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/utils/assert_no_args_after_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | import 'has_args_after_separator.dart'; 4 | 5 | void assertNoArgsAfterSeparator( 6 | ArgResults argResults, void Function(String msg) usageException, 7 | {String? commandName, String? usageFooter}) { 8 | if (hasArgsAfterSeparator(argResults)) { 9 | usageException('${commandName != null ? 'The "$commandName"' : 'This'} ' 10 | 'command does not support args after a separator.' 11 | '${usageFooter != null ? '\n$usageFooter' : ''}'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/utils/assert_no_positional_args_before_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | import 'has_any_positional_args_before_separator.dart'; 4 | 5 | void assertNoPositionalArgs( 6 | String name, 7 | ArgResults argResults, 8 | void Function(String message) usageException, { 9 | bool beforeSeparator = false, 10 | }) { 11 | if (hasAnyPositionalArgsBeforeSeparator(argResults)) { 12 | usageException('The "$name" command does not support positional args' 13 | '${beforeSeparator ? ' before the `--` separator' : ''}.\n'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/utils/assert_no_positional_args_nor_args_after_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | import 'has_args_after_separator.dart'; 4 | 5 | void assertNoPositionalArgsNorArgsAfterSeparator( 6 | ArgResults argResults, void Function(String msg) usageException, 7 | {String? commandName, String? usageFooter, bool allowRest = false}) { 8 | if ((argResults.rest.isNotEmpty && !allowRest) || 9 | hasArgsAfterSeparator(argResults)) { 10 | usageException('${commandName != null ? 'The "$commandName"' : 'This'} ' 11 | 'command does not support positional args nor args after a separator.' 12 | '${usageFooter != null ? '\n$usageFooter' : ''}'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/utils/cached_pubspec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:path/path.dart' as p; 4 | import 'package:pubspec_parse/pubspec_parse.dart'; 5 | 6 | Pubspec cachedPubspec({String? path}) { 7 | final sourceUrl = p.join(path ?? p.current, 'pubspec.yaml'); 8 | return _cachedPubspecs.putIfAbsent( 9 | sourceUrl, 10 | () => Pubspec.parse(File(sourceUrl).readAsStringSync(), 11 | sourceUrl: Uri.parse(sourceUrl))); 12 | } 13 | 14 | final _cachedPubspecs = {}; 15 | -------------------------------------------------------------------------------- /lib/src/utils/dart_dev_paths.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart' as p; 2 | 3 | /// A collection of paths to files and directories constructed to be compatible 4 | /// with a given [p.Context]. 5 | class DartDevPaths { 6 | final p.Context _context; 7 | 8 | DartDevPaths({p.Context? context}) : _context = context ?? p.context; 9 | 10 | String cache([String? subPath]) => _context.normalize( 11 | _context.joinAll([..._cacheParts, if (subPath != null) subPath])); 12 | 13 | String get _cacheForDart => p.url.joinAll(_cacheParts); 14 | 15 | final List _cacheParts = ['.dart_tool', 'dart_dev']; 16 | 17 | String get config => _context.joinAll(_configParts); 18 | 19 | String get configForDart => p.url.joinAll(_configParts); 20 | 21 | final List _configParts = ['tool', 'dart_dev', 'config.dart']; 22 | 23 | String get configFromRunScriptForDart => p.url.relative( 24 | p.url.absolute(configForDart), 25 | from: p.url.absolute(_cacheForDart), 26 | ); 27 | 28 | String get packageConfig => 29 | _context.join('.dart_tool', 'package_config.json'); 30 | 31 | String get legacyConfig => _context.join('tool', 'dev.dart'); 32 | 33 | String get runScript => cache('run.dart'); 34 | 35 | String get runExecutable => cache('run'); 36 | 37 | String get runExecutableDigest => cache('run.digest'); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/utils/dart_semver_version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pub_semver/pub_semver.dart'; 4 | 5 | final versionPattern = RegExp(r'(\d+.\d+.\d+)'); 6 | 7 | Version get dartSemverVersion => 8 | Version.parse(versionPattern.firstMatch(Platform.version)!.group(1)!); 9 | 10 | bool get dartVersionHasDartanalyzer => 11 | dartSemverVersion < Version.parse('2.18.0'); 12 | 13 | bool get dartVersionHasDartfmt => dartSemverVersion < Version.parse('2.15.0'); 14 | -------------------------------------------------------------------------------- /lib/src/utils/dart_tool_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_dev/src/utils/dart_dev_paths.dart'; 4 | 5 | void createCacheDir({String? subPath}) { 6 | final dir = Directory(DartDevPaths().cache(subPath)); 7 | if (!dir.existsSync()) { 8 | dir.createSync(recursive: true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/utils/ensure_process_exit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logging/logging.dart'; 4 | 5 | import 'exit_process_signals.dart'; 6 | 7 | /// Ensures that the current process does not exit until the given [process] 8 | /// exits. 9 | /// 10 | /// This function prevents the current process from exiting by watching the exit 11 | /// signals for the current platform (SIGINT on Windows, SIGINT and SIGTERM for 12 | /// other platforms) and consuming them until [process] exits. 13 | /// 14 | /// If [forwardExitSignals] is true, the exit signals received by the current 15 | /// process will be forwarded via [process.kill]. This is only needed if the 16 | /// given [process] was started in either the [ProcessStartMode.detached] or 17 | /// [ProcessStartMode.detachedWithStdio] modes. 18 | void ensureProcessExit(Process process, 19 | {bool forwardExitSignals = false, Logger? log}) { 20 | final signalsSub = exitProcessSignals.listen((signal) async { 21 | log?.info('Waiting for subprocess to exit...'); 22 | if (forwardExitSignals) { 23 | process.kill(signal); 24 | } 25 | }); 26 | process.exitCode.then((_) { 27 | signalsSub.cancel(); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/utils/executables.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | final dart = 'dart'; 4 | 5 | final dartanalyzer = Platform.isWindows ? 'dartanalyzer.bat' : 'dartanalyzer'; 6 | 7 | final dartfmt = Platform.isWindows ? 'dartfmt.bat' : 'dartfmt'; 8 | -------------------------------------------------------------------------------- /lib/src/utils/exit_process_signals.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:async/async.dart'; 5 | 6 | Stream get exitProcessSignals => Platform.isWindows 7 | ? ProcessSignal.sigint.watch() 8 | : StreamGroup.merge( 9 | [ProcessSignal.sigterm.watch(), ProcessSignal.sigint.watch()]); 10 | -------------------------------------------------------------------------------- /lib/src/utils/format_tool_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/ast/ast.dart'; 2 | import 'package:analyzer/dart/ast/visitor.dart'; 3 | import 'package:collection/collection.dart' show IterableExtension; 4 | import 'package:dart_dev/src/tools/over_react_format_tool.dart'; 5 | 6 | import '../../dart_dev.dart'; 7 | import 'logging.dart'; 8 | 9 | /// Visits a `dart_dev/config.dart` file and searches for a custom formatter. 10 | /// 11 | /// When a custom formatter is found, this will look for possible configuration options 12 | /// and reconstruct them on a new formatter instance. 13 | /// 14 | /// NOTE: Because the visitor doesn't have access to the scope of the configuration 15 | /// being parsed, most values need to be a literal type (ListLiteral, StringLiteral, IntegerLiteral) 16 | /// to be reconstructed. 17 | /// 18 | /// Expects the configuration to be in the format of: 19 | /// ```dart 20 | /// final config = { 21 | /// 'format': OverReactFormatTool() 22 | /// ..lineLength = 120, 23 | /// }; 24 | /// ``` 25 | class FormatToolBuilder extends GeneralizingAstVisitor { 26 | DevTool? formatDevTool; 27 | 28 | bool failedToDetectAKnownFormatter = false; 29 | 30 | @override 31 | visitMapLiteralEntry(MapLiteralEntry node) { 32 | super.visitMapLiteralEntry(node); 33 | 34 | String mapEntryKey = node.key.toSource().replaceAll("'", ""); 35 | 36 | if (mapEntryKey != 'format') return; 37 | 38 | final formatterInvocation = node.value; 39 | formatDevTool = detectFormatter(formatterInvocation); 40 | 41 | if (formatDevTool == null) { 42 | failedToDetectAKnownFormatter = true; 43 | return; 44 | } 45 | 46 | if (formatterInvocation is CascadeExpression) { 47 | AssignmentExpression? getCascadeByProperty(String property) { 48 | return formatterInvocation.cascadeSections 49 | .whereType() 50 | .firstWhereOrNull((assignment) { 51 | final lhs = assignment.leftHandSide; 52 | return lhs is PropertyAccess && lhs.propertyName.name == property; 53 | }); 54 | } 55 | 56 | final typedFormatDevTool = formatDevTool; 57 | if (typedFormatDevTool is FormatTool) { 58 | final formatter = getCascadeByProperty('formatter'); 59 | if (formatter != null) { 60 | final formatterType = formatter.rightHandSide; 61 | if (formatterType is PrefixedIdentifier) { 62 | final detectedFormatter = 63 | detectFormatterForFormatTool(formatterType.identifier); 64 | if (detectedFormatter != null) { 65 | typedFormatDevTool.formatter = detectedFormatter; 66 | } 67 | } else { 68 | logWarningMessageFor(KnownErrorOutcome.failedToParseFormatter); 69 | } 70 | } 71 | 72 | final formatterArgs = getCascadeByProperty('formatterArgs'); 73 | if (formatterArgs != null) { 74 | final argList = formatterArgs.rightHandSide; 75 | if (argList is ListLiteral) { 76 | final stringArgs = argList.elements 77 | .whereType() 78 | .where((e) => e.stringValue != null) 79 | .map((e) => e.stringValue!) 80 | .toList(); 81 | typedFormatDevTool.formatterArgs = stringArgs; 82 | 83 | if (stringArgs.length < argList.elements.length) { 84 | logWarningMessageFor( 85 | KnownErrorOutcome.failedToReconstructFormatterArgs); 86 | } 87 | } else { 88 | logWarningMessageFor(KnownErrorOutcome.failedToParseFormatterArgs); 89 | } 90 | } 91 | 92 | final organizeDirectives = getCascadeByProperty('organizeDirectives'); 93 | if (organizeDirectives != null) { 94 | final valueExpression = organizeDirectives.rightHandSide; 95 | if (valueExpression is BooleanLiteral) { 96 | typedFormatDevTool.organizeDirectives = valueExpression.value; 97 | } else { 98 | logWarningMessageFor( 99 | KnownErrorOutcome.failedToParseOrganizeDirective); 100 | } 101 | } 102 | } else if (typedFormatDevTool is OverReactFormatTool) { 103 | final lineLengthAssignment = getCascadeByProperty('lineLength'); 104 | if (lineLengthAssignment != null) { 105 | final lengthExpression = lineLengthAssignment.rightHandSide; 106 | if (lengthExpression is IntegerLiteral) { 107 | typedFormatDevTool.lineLength = lengthExpression.value; 108 | } else { 109 | logWarningMessageFor(KnownErrorOutcome.failedToParseLineLength); 110 | } 111 | } 112 | 113 | final organizeDirectivesAssignment = 114 | getCascadeByProperty('organizeDirectives'); 115 | if (organizeDirectivesAssignment != null) { 116 | final valueExpression = organizeDirectivesAssignment.rightHandSide; 117 | if (valueExpression is BooleanLiteral) { 118 | typedFormatDevTool.organizeDirectives = valueExpression.value; 119 | } else { 120 | logWarningMessageFor( 121 | KnownErrorOutcome.failedToParseOrganizeDirective); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | enum KnownErrorOutcome { 130 | failedToParseFormatter, 131 | failedToReconstructFormatterArgs, 132 | failedToParseFormatterArgs, 133 | failedToParseLineLength, 134 | failedToParseOrganizeDirective, 135 | } 136 | 137 | void logWarningMessageFor(KnownErrorOutcome outcome) { 138 | String? errorMessage; 139 | 140 | switch (outcome) { 141 | case KnownErrorOutcome.failedToParseFormatter: 142 | errorMessage = '''Failed to parse the formatter configuration. 143 | 144 | This is likely because the assigned value is not in the form `Formatter.`. 145 | '''; 146 | break; 147 | case KnownErrorOutcome.failedToReconstructFormatterArgs: 148 | errorMessage = 149 | '''Failed to reconstruct all items in the formatterArgs list. 150 | 151 | This is likely because the list contained types that were not StringLiterals. 152 | '''; 153 | break; 154 | case KnownErrorOutcome.failedToParseFormatterArgs: 155 | errorMessage = '''Failed to parse the formatterArgs list. 156 | 157 | This is likely because the list is not a ListLiteral. 158 | '''; 159 | break; 160 | case KnownErrorOutcome.failedToParseLineLength: 161 | errorMessage = '''Failed to parse the line-length configuration. 162 | 163 | This is likely because assignment does not use an IntegerLiteral. 164 | '''; 165 | break; 166 | case KnownErrorOutcome.failedToParseOrganizeDirective: 167 | errorMessage = '''Failed to parse the organizeDirectives configuration. 168 | 169 | This is likely because assignment does not use an BooleanLiteral. 170 | '''; 171 | break; 172 | } 173 | 174 | log.warning(errorMessage); 175 | } 176 | 177 | Formatter? detectFormatterForFormatTool(SimpleIdentifier formatterIdentifier) { 178 | Formatter? formatter; 179 | 180 | switch (formatterIdentifier.name) { 181 | case 'dartfmt': 182 | formatter = Formatter.dartfmt; 183 | break; 184 | case 'dartFormat': 185 | formatter = Formatter.dartFormat; 186 | break; 187 | case 'dartStyle': 188 | formatter = Formatter.dartStyle; 189 | break; 190 | default: 191 | break; 192 | } 193 | 194 | return formatter; 195 | } 196 | 197 | DevTool? detectFormatter(AstNode formatterNode) { 198 | String? detectedFormatterName; 199 | DevTool? tool; 200 | 201 | if (formatterNode is MethodInvocation) { 202 | detectedFormatterName = formatterNode.methodName.name; 203 | } else if (formatterNode is CascadeExpression) { 204 | detectedFormatterName = 205 | formatterNode.target.toSource().replaceAll(RegExp('[()]'), ''); 206 | } 207 | 208 | if (detectedFormatterName == 'FormatTool') { 209 | tool = FormatTool(); 210 | } else if (detectedFormatterName == 'OverReactFormatTool') { 211 | tool = OverReactFormatTool(); 212 | } 213 | 214 | return tool; 215 | } 216 | -------------------------------------------------------------------------------- /lib/src/utils/get_dart_version_comment.dart: -------------------------------------------------------------------------------- 1 | /// Returns the Dart version comment contained in a given [source] file, 2 | /// or `null` if one does not exist. 3 | /// 4 | /// Uses regex over the analyzer for performance. 5 | String? getDartVersionComment(String source) => 6 | RegExp(r'^//\s*@dart\s*=.+$', multiLine: true).firstMatch(source)?.group(0); 7 | -------------------------------------------------------------------------------- /lib/src/utils/global_package_is_active_and_compatible.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:pub_semver/pub_semver.dart'; 5 | 6 | import 'executables.dart' as exe; 7 | 8 | /// Returns `true` if [packageName] is globally activated at a version 9 | /// allowed by [constraint]. Returns `false` otherwise. 10 | /// 11 | /// This is determined by running a `dart pub global list` and looking for 12 | /// [packageName] in the output and then testing its version against 13 | /// [constraint]. 14 | /// 15 | /// The pub-cache that gets checked during this can be overridden by providing 16 | /// an [environment] map with a `'PUB_CACHE': ''` entry, which will be 17 | /// passed to the [Process] that is run by this function. 18 | bool globalPackageIsActiveAndCompatible( 19 | String packageName, VersionConstraint constraint, 20 | {Map? environment}) { 21 | final args = ['pub', 'global', 'list']; 22 | final result = Process.runSync(exe.dart, args, 23 | environment: environment, stderrEncoding: utf8, stdoutEncoding: utf8); 24 | if (result.exitCode != 0) { 25 | throw ProcessException( 26 | exe.dart, 27 | args, 28 | 'Could not list global pub packages:\n${result.stderr}', 29 | result.exitCode); 30 | } 31 | 32 | for (final line in result.stdout.split('\n')) { 33 | // Example line: "webdev 2.5.1" or "dart_dev 3.0.0 at path ..." 34 | final parts = line.split(' '); 35 | if (parts.length < 2 || parts[0] != packageName) { 36 | continue; 37 | } 38 | return constraint.allows(Version.parse(parts[1])); 39 | } 40 | return false; 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/utils/has_any_positional_args_before_separator.dart: -------------------------------------------------------------------------------- 1 | // The utility in this file was originally modeled after: 2 | // https://github.com/dart-lang/build/blob/14747dbf8c2f1bb44f89e2cb0726744278a23685/build_runner/lib/src/entrypoint/run_script.dart#L35-L63 3 | 4 | import 'package:args/args.dart'; 5 | 6 | /// Returns `true` if any positional args are found in [argResults] before the 7 | /// `--` separator, and `false` otherise. 8 | bool hasAnyPositionalArgsBeforeSeparator(ArgResults argResults) { 9 | if (argResults.rest.isEmpty) { 10 | return false; 11 | } 12 | 13 | final separatorPos = argResults.arguments.indexOf('--'); 14 | if (separatorPos < 0) { 15 | // `argResults.rest` is _not_ empty, but there is no `--` separator, so 16 | // there must be positional args before the (nonexistant) separator. 17 | return true; 18 | } 19 | 20 | final expectedRest = argResults.arguments.skip(separatorPos + 1).toList(); 21 | if (argResults.rest.length != expectedRest.length) { 22 | // There number of args in the raw arguments list after the `--` separator 23 | // does not match the number of args in `argResults.rest`, which means there 24 | // must be some before the `--` separator. 25 | return true; 26 | } 27 | for (var i = 0; i < argResults.rest.length; i++) { 28 | if (expectedRest[i] != argResults.rest[i]) { 29 | // The expected arguments from the raw arguments list after the `--` 30 | // separator did not match exactly those found in `argResults.rest`. 31 | return true; 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/utils/has_args_after_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | bool hasArgsAfterSeparator(ArgResults argResults) { 4 | final sepPos = argResults.arguments.indexOf('--'); 5 | return sepPos != -1 && sepPos != argResults.arguments.length - 1; 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/utils/logging.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The logging utility in this file was originally modeled after: 16 | // https://github.com/dart-lang/build/blob/0e79b63c6387adbb7e7f4c4f88d572b1242d24df/build_runner/lib/src/logging/std_io_logging.dart 17 | 18 | import 'dart:async'; 19 | import 'dart:convert' as convert; 20 | import 'dart:io' as io; 21 | 22 | import 'package:io/ansi.dart'; 23 | import 'package:logging/logging.dart'; 24 | import 'package:stack_trace/stack_trace.dart'; 25 | 26 | import 'parse_flag_from_args.dart'; 27 | 28 | // Ensures this message does not get overwritten by later logs. 29 | const _logSuffix = '\n'; 30 | 31 | final log = Logger('DartDev'); 32 | 33 | void attachLoggerToStdio(List args) { 34 | final verbose = parseFlagFromArgs(args, 'verbose', abbr: 'v'); 35 | Logger.root.level = verbose ? Level.ALL : Level.INFO; 36 | Logger.root.onRecord.listen(stdIOLogListener(verbose: verbose)); 37 | } 38 | 39 | StringBuffer colorLog(LogRecord record, {bool verbose = false}) { 40 | AnsiCode color; 41 | if (record.level < Level.WARNING) { 42 | color = cyan; 43 | } else if (record.level < Level.SEVERE) { 44 | color = yellow; 45 | } else { 46 | color = red; 47 | } 48 | final level = color.wrap('[${record.level}]'); 49 | final eraseLine = ansiOutputEnabled && !verbose ? '\x1b[2K\r' : ''; 50 | final lines = [ 51 | '$eraseLine$level ${_loggerName(record, verbose)}${record.message}' 52 | ]; 53 | 54 | if (record.error != null) { 55 | lines.add(record.error!); 56 | } 57 | 58 | if (record.stackTrace != null && verbose) { 59 | final trace = Trace.from(record.stackTrace!).terse; 60 | lines.add(trace); 61 | } 62 | 63 | final message = StringBuffer(lines.join('\n')); 64 | 65 | // We always add an extra newline at the end of each message, so it 66 | // isn't multiline unless we see > 2 lines. 67 | final multiLine = convert.LineSplitter.split(message.toString()).length > 2; 68 | 69 | if (record.level > Level.INFO || !ansiOutputEnabled || multiLine || verbose) { 70 | if (!lines.last.toString().endsWith('\n')) { 71 | // Add a newline to the output so the last line isn't written over. 72 | message.writeln(''); 73 | } 74 | } 75 | return message; 76 | } 77 | 78 | /// Returns a human readable string for a duration. 79 | /// 80 | /// Handles durations that span up to hours - this will not be a good fit for 81 | /// durations that are longer than days. 82 | /// 83 | /// Always attempts 2 'levels' of precision. Will show hours/minutes, 84 | /// minutes/seconds, seconds/tenths of a second, or milliseconds depending on 85 | /// the largest level that needs to be displayed. 86 | String humanReadable(Duration duration) { 87 | if (duration < const Duration(seconds: 1)) { 88 | return '${duration.inMilliseconds}ms'; 89 | } 90 | if (duration < const Duration(minutes: 1)) { 91 | return '${(duration.inMilliseconds / 1000.0).toStringAsFixed(1)}s'; 92 | } 93 | if (duration < const Duration(hours: 1)) { 94 | final minutes = duration.inMinutes; 95 | final remaining = duration - Duration(minutes: minutes); 96 | return '${minutes}m ${remaining.inSeconds}s'; 97 | } 98 | final hours = duration.inHours; 99 | final remaining = duration - Duration(hours: hours); 100 | return '${hours}h ${remaining.inMinutes}m'; 101 | } 102 | 103 | void logSubprocessHeader(Logger logger, String command, 104 | {Level level = Level.INFO}) { 105 | final numColumns = io.stdout.hasTerminal ? io.stdout.terminalColumns : 79; 106 | logger.log(level, 107 | 'Running subprocess:\n${magenta.wrap(command)}\n${'-' * numColumns}\n'); 108 | } 109 | 110 | /// Logs an asynchronous [action] with [description] before and after. 111 | /// 112 | /// Returns a future that completes after the action and logging finishes. 113 | Future logTimedAsync( 114 | Logger logger, 115 | String description, 116 | Future Function() action, { 117 | Level level = Level.INFO, 118 | }) async { 119 | final watch = Stopwatch()..start(); 120 | logger.log(level, '$description...'); 121 | final result = await action(); 122 | watch.stop(); 123 | final time = '${humanReadable(watch.elapsed)}$_logSuffix'; 124 | logger.log(level, '$description completed, took $time'); 125 | return result; 126 | } 127 | 128 | /// Logs a synchronous [action] with [description] before and after. 129 | /// 130 | /// Returns a future that completes after the action and logging finishes. 131 | T logTimedSync( 132 | Logger logger, 133 | String description, 134 | T Function() action, { 135 | Level level = Level.INFO, 136 | }) { 137 | final watch = Stopwatch()..start(); 138 | logger.log(level, '$description...'); 139 | final result = action(); 140 | watch.stop(); 141 | final time = '${humanReadable(watch.elapsed)}$_logSuffix'; 142 | logger.log(level, '$description completed, took $time'); 143 | return result; 144 | } 145 | 146 | void Function(LogRecord) stdIOLogListener({bool verbose = false}) => 147 | (record) => io.stdout.write(record.message.trim().isEmpty 148 | ? '\n' * (record.message.split('\n').length - 1) 149 | : colorLog(record, verbose: verbose)); 150 | 151 | String _loggerName(LogRecord record, bool verbose) { 152 | final maybeSplit = record.level >= Level.WARNING ? '\n' : ' '; 153 | return verbose ? '${record.loggerName}:$maybeSplit' : ''; 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/utils/organize_directives/namespace.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/ast/ast.dart'; 2 | import 'package:analyzer/dart/ast/token.dart'; 3 | 4 | /// A representation of an namespace directive. 5 | /// 6 | /// Capable of tracking comments that should be associated with an namespace 7 | /// during organization (which cannot be represented by the AST) 8 | class Namespace { 9 | /// The AST node that represents an namespace in a file. 10 | final NamespaceDirective directive; 11 | 12 | /// Comments that appear before the namespace that should stay with the 13 | /// namespace when organized. 14 | List beforeComments = []; 15 | 16 | /// Comments that appear after the namespace that should stay with the 17 | /// namespace when organized. 18 | List afterComments = []; 19 | 20 | /// The file being imported/exported. 21 | String get target { 22 | return directive.uri.stringValue!; 23 | } 24 | 25 | /// If the namespace is a dart namespace. Memoized for performance. 26 | bool? _isDart; 27 | bool get isDart { 28 | return _isDart ??= target.startsWith('dart:'); 29 | } 30 | 31 | /// If the namespace is an external package namespace. Memoized for performance. 32 | bool? _isExternalPkg; 33 | bool get isExternalPkg { 34 | return _isExternalPkg ??= target.startsWith('package:'); 35 | } 36 | 37 | /// If the namespace is a relative namespace. Memoized for performance. 38 | bool? _isRelative; 39 | bool get isRelative { 40 | return _isRelative ??= !isExternalPkg && !isDart; 41 | } 42 | 43 | Namespace(this.directive); 44 | 45 | /// The character offset of the start of the namespace statement in source text. 46 | /// Excludes comments associated with this namespace. 47 | int get statementStart => directive.beginToken.charOffset; 48 | 49 | /// The character offset of the end of the namespace statement in source text. 50 | /// Excludes comments associated with this namespace. 51 | int get statementEnd => directive.endToken.end; 52 | 53 | /// The character offset of the end of this namespace in source text. 54 | /// Includes comments associated with this namespace. 55 | int end() { 56 | var end = directive.endToken.end; 57 | for (final afterComment in afterComments) { 58 | if (afterComment.end > end) { 59 | end = afterComment.end; 60 | } 61 | } 62 | return end; 63 | } 64 | 65 | /// The character offset of the start of this namespace in source text. 66 | /// Includes comments associated with this namespace. 67 | int start() { 68 | var charOffset = directive.beginToken.charOffset; 69 | for (final beforeComment in beforeComments) { 70 | if (beforeComment.charOffset < charOffset) { 71 | charOffset = beforeComment.charOffset; 72 | } 73 | } 74 | return charOffset; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/utils/organize_directives/namespace_collector.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/ast/ast.dart'; 2 | import 'package:analyzer/dart/ast/visitor.dart'; 3 | 4 | /// A visitor which collects all imports and exports. 5 | /// 6 | /// Imports and exports are returned in the order they appear in a file when 7 | /// passed to `CompilationUnit.accept`. 8 | class NamespaceCollector extends SimpleAstVisitor> { 9 | final List _namespaces = []; 10 | 11 | @override 12 | List? visitExportDirective(ExportDirective node) { 13 | _namespaces.add(node); 14 | return null; 15 | } 16 | 17 | @override 18 | List? visitImportDirective(ImportDirective node) { 19 | _namespaces.add(node); 20 | return null; 21 | } 22 | 23 | @override 24 | List visitCompilationUnit(CompilationUnit node) { 25 | node.visitChildren(this); 26 | return _namespaces; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/utils/organize_directives/organize_directives.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/utilities.dart'; 2 | import 'package:analyzer/dart/ast/ast.dart'; 3 | import 'package:analyzer/dart/ast/token.dart'; 4 | 5 | import 'namespace.dart'; 6 | import 'namespace_collector.dart'; 7 | 8 | /// Takes in a file as a string and organizes the imports and exports. 9 | /// Sorts imports/exports and removes double quotes. 10 | /// 11 | /// Throws an ArgumentError if [sourceFileContents] cannot be parsed. 12 | String organizeDirectives(String sourceFileContents) { 13 | final directives = parseString(content: sourceFileContents) 14 | .unit 15 | .accept(NamespaceCollector())!; 16 | 17 | if (directives.isEmpty) { 18 | return sourceFileContents; 19 | } 20 | 21 | final namespaces = _assignCommentsInFileToNamespaceDirective( 22 | sourceFileContents, 23 | directives, 24 | ); 25 | final sortedDirectives = _organizeDirectives(sourceFileContents, namespaces); 26 | return _replaceDirectives(sourceFileContents, namespaces, sortedDirectives); 27 | } 28 | 29 | /// Replaces the namespace directives in a source file with a given string. 30 | /// 31 | /// Namespaces should be sorted in the order they appear in the source file. 32 | String _replaceDirectives( 33 | String sourceFileContents, 34 | List namespaces, 35 | String replaceString, 36 | ) { 37 | final firstNamespaceStartIdx = namespaces.first.start(); 38 | final lastNamespaceEndIdx = namespaces.last.end(); 39 | return sourceFileContents.replaceRange( 40 | firstNamespaceStartIdx, 41 | lastNamespaceEndIdx + 1, 42 | replaceString, 43 | ); 44 | } 45 | 46 | /// Returns a sorted string of namespace directives. 47 | String _organizeDirectives( 48 | String sourceFileContents, 49 | List namespaces, 50 | ) { 51 | final sortedImports = _organizeDirectivesOfType( 52 | sourceFileContents, 53 | namespaces, 54 | ); 55 | final sortedExports = _organizeDirectivesOfType( 56 | sourceFileContents, 57 | namespaces, 58 | ); 59 | 60 | return [ 61 | if (sortedImports.isNotEmpty) sortedImports, 62 | if (sortedExports.isNotEmpty) sortedExports 63 | ].join('\n'); 64 | } 65 | 66 | /// Returns a sorted string of namespace directives of a given type. 67 | String _organizeDirectivesOfType( 68 | String sourceFileContents, 69 | List namespaces, 70 | ) { 71 | final directives = 72 | namespaces.where((element) => element.directive is T).toList(); 73 | 74 | directives.sort(_namespaceComparator); 75 | 76 | return _getSortedNamespaceString(sourceFileContents, directives); 77 | } 78 | 79 | /// Puts comments in a source file with the correct namespace directive so they 80 | /// can be moved with the directive when sorted. 81 | /// 82 | /// The parser puts "precedingComments" on each token. However, a directive's 83 | /// precedingComments shouldn't necessarily be the comments that move with the 84 | /// directive during a sort. If a directive has a trailing comment on the same 85 | /// line as a directive, it will be attached to the next Token's 86 | /// "precedingComments". 87 | /// 88 | /// For this reason, we go thru all directives (and the first token after the 89 | /// last directive), look at their precedingComments, and determine which 90 | /// directive they belong to. 91 | List _assignCommentsInFileToNamespaceDirective( 92 | String sourceFileContents, 93 | List directives, 94 | ) { 95 | final namespaces = []; 96 | 97 | Namespace? prevNamespace; 98 | for (var directive in directives) { 99 | final currNamespace = Namespace(directive); 100 | namespaces.add(currNamespace); 101 | 102 | _assignCommentsBeforeTokenToNamespace( 103 | currNamespace.directive.beginToken, 104 | sourceFileContents, 105 | prevNamespace, 106 | currNamespace: currNamespace, 107 | ); 108 | 109 | prevNamespace = currNamespace; 110 | } 111 | 112 | // Assign comments after the last directive to the last directive if they are 113 | // on the same line. 114 | _assignCommentsBeforeTokenToNamespace( 115 | prevNamespace!.directive.endToken.next!, 116 | sourceFileContents, 117 | prevNamespace, 118 | ); 119 | return namespaces; 120 | } 121 | 122 | /// Assigns comments before [token] to [prevNamespace] if on the same line as 123 | /// [prevNamespace]. Else it assigns the comment to [currNamespace], if it exists. 124 | void _assignCommentsBeforeTokenToNamespace( 125 | Token token, 126 | String sourceFileContents, 127 | Namespace? prevNamespace, { 128 | Namespace? currNamespace, 129 | }) { 130 | // `precedingComments` returns the first comment before token. 131 | // Calling `comment.next` returns the next comment. 132 | // Returns null when there are no more comments left. 133 | for (Token? comment = token.precedingComments; 134 | comment != null; 135 | comment = comment.next) { 136 | // the LanguageVersionToken (`// @dart=2.11`) must stay where it is at 137 | // the top of the file. Do not assign this to any namespace 138 | if (comment is LanguageVersionToken) continue; 139 | 140 | if (_commentIsOnSameLineAsNamespace( 141 | comment, 142 | prevNamespace, 143 | sourceFileContents, 144 | )) { 145 | prevNamespace!.afterComments.add(comment); 146 | } else if (currNamespace != null) { 147 | currNamespace.beforeComments.add(comment); 148 | } 149 | } 150 | } 151 | 152 | /// Checks if a given comment is on the same line as a directive. 153 | /// It's expected that directive end is before comment start. 154 | bool _commentIsOnSameLineAsNamespace( 155 | Token comment, Namespace? namespace, String sourceFileContents) { 156 | return namespace != null && 157 | !sourceFileContents 158 | .substring(namespace.directive.endToken.end, comment.charOffset) 159 | .contains('\n'); 160 | } 161 | 162 | /// Converts a list of sorted namespaces into a plain text string. 163 | String _getSortedNamespaceString( 164 | String sourceFileContents, 165 | List namespaces, 166 | ) { 167 | final sortedReplacement = StringBuffer(); 168 | final firstRelativeNamespaceIdx = 169 | namespaces.indexWhere((namespace) => namespace.isRelative); 170 | final firstPkgDirectiveIdx = 171 | namespaces.indexWhere((namespace) => namespace.isExternalPkg); 172 | for (var nsIndex = 0; nsIndex < namespaces.length; nsIndex++) { 173 | final namespace = namespaces[nsIndex]; 174 | if (nsIndex != 0 && 175 | (nsIndex == firstRelativeNamespaceIdx || 176 | nsIndex == firstPkgDirectiveIdx)) { 177 | sortedReplacement.write('\n'); 178 | } 179 | final namespaceWithQuotesReplaced = sourceFileContents 180 | .substring(namespace.statementStart, namespace.statementEnd) 181 | .replaceAll('"', "'"); 182 | 183 | final source = sourceFileContents 184 | .replaceRange( 185 | namespace.statementStart, 186 | namespace.statementEnd, 187 | namespaceWithQuotesReplaced, 188 | ) 189 | .substring(namespace.start(), namespace.end()); 190 | sortedReplacement 191 | ..write(source) 192 | ..write('\n'); 193 | } 194 | return sortedReplacement.toString(); 195 | } 196 | 197 | /// A comparator that will sort dart directives first, then package directives, 198 | /// then relative directives. 199 | int _namespaceComparator(Namespace first, Namespace second) { 200 | if (first.isDart && second.isDart) { 201 | return first.target.compareTo(second.target); 202 | } 203 | 204 | if (first.isDart && !second.isDart) { 205 | return -1; 206 | } 207 | 208 | if (!first.isDart && second.isDart) { 209 | return 1; 210 | } 211 | 212 | // Neither are dart directives 213 | final firstIsPkg = first.isExternalPkg; 214 | final secondIsPkg = second.isExternalPkg; 215 | if (firstIsPkg && secondIsPkg) { 216 | return first.target.compareTo(second.target); 217 | } 218 | 219 | if (firstIsPkg && !secondIsPkg) { 220 | return -1; 221 | } 222 | 223 | if (!firstIsPkg && secondIsPkg) { 224 | return 1; 225 | } 226 | 227 | // Neither are dart directives or pkg directives. Must be relative path directives... 228 | return first.target.compareTo(second.target); 229 | } 230 | -------------------------------------------------------------------------------- /lib/src/utils/organize_directives/organize_directives_in_paths.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:io/ansi.dart'; 4 | 5 | import 'organize_directives.dart'; 6 | 7 | /// Organizes imports/exports in a list of files and directories. 8 | int organizeDirectivesInPaths( 9 | Iterable paths, { 10 | bool check = false, 11 | bool verbose = false, 12 | }) { 13 | final allFiles = _getAllFiles(paths); 14 | return _organizeDirectivesInFiles(allFiles, verbose: verbose, check: check); 15 | } 16 | 17 | /// Returns all file paths from a given set of files and directories. 18 | Iterable _getAllFiles(Iterable paths) { 19 | final allFiles = {}; 20 | 21 | for (final path in paths) { 22 | switch (FileSystemEntity.typeSync(path)) { 23 | case FileSystemEntityType.directory: 24 | // skip hidden directories 25 | if (path.startsWith('.')) { 26 | continue; 27 | } 28 | final children = Directory(path).listSync().map((e) => e.path); 29 | allFiles.addAll(_getAllFiles(children)); 30 | break; 31 | case FileSystemEntityType.file: 32 | if (File(path).path.endsWith('.dart')) { 33 | allFiles.add(path); 34 | } 35 | break; 36 | case FileSystemEntityType.link: 37 | // skip links 38 | break; 39 | default: 40 | throw Exception('Unknown FileSystemEntity type encountered'); 41 | } 42 | } 43 | 44 | return allFiles; 45 | } 46 | 47 | /// Organizes imports/exports in a list of files. 48 | int _organizeDirectivesInFiles( 49 | Iterable paths, { 50 | bool verbose = false, 51 | bool check = false, 52 | }) { 53 | var exitCode = 0; 54 | for (final path in paths) { 55 | final codeForFile = 56 | _organizeDirectivesInFile(path, verbose: verbose, check: check); 57 | if (codeForFile != 0) { 58 | exitCode = codeForFile; 59 | } 60 | } 61 | return exitCode; 62 | } 63 | 64 | /// Organizes imports/exports in a file. 65 | int _organizeDirectivesInFile( 66 | String filePath, { 67 | bool verbose = false, 68 | bool check = false, 69 | }) { 70 | final file = File(filePath); 71 | final fileContents = _safelyReadFileContents(file); 72 | if (fileContents == null) { 73 | return _fail( 74 | '$filePath not found. Skipping import/export organization for file.', 75 | ); 76 | } 77 | 78 | final fileWithOrganizedDirectives = _safelyOrganizeDirectives(fileContents); 79 | if (fileWithOrganizedDirectives == null) { 80 | return _fail( 81 | '$filePath has syntax errors. Please fix syntax errors and try again.', 82 | ); 83 | } 84 | 85 | final fileChanged = fileWithOrganizedDirectives != fileContents; 86 | if (fileChanged && check) { 87 | return _fail('$filePath has imports/exports that need to be organized.'); 88 | } else if (fileChanged && 89 | !_safelyWriteFile(file, fileWithOrganizedDirectives)) { 90 | return _fail( 91 | '$filePath encountered a FileSystemException while writing output.', 92 | ); 93 | } 94 | 95 | if (verbose && !check) { 96 | print(green.wrap('$filePath successfully organized imports/exports')); 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | int _fail(String message) { 103 | print(red.wrap(message)); 104 | return 1; 105 | } 106 | 107 | String? _safelyReadFileContents(File file) { 108 | try { 109 | return file.readAsStringSync(); 110 | } on FileSystemException { 111 | return null; 112 | } 113 | } 114 | 115 | String? _safelyOrganizeDirectives(String fileContents) { 116 | try { 117 | return organizeDirectives(fileContents); 118 | } on ArgumentError { 119 | return null; 120 | } 121 | } 122 | 123 | bool _safelyWriteFile(File file, String fileContents) { 124 | try { 125 | file.writeAsStringSync(fileContents); 126 | return true; 127 | } on FileSystemException { 128 | return false; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/utils/package_is_immediate_dependency.dart: -------------------------------------------------------------------------------- 1 | import 'cached_pubspec.dart'; 2 | 3 | /// Returns `true` if [packageName] is an immediate dependency, and `false` 4 | /// otherwise. 5 | /// 6 | /// This is useful for determining whether an executable from [packageName] can 7 | /// be run (pub requires that the package be an explicit dependency to do so) or 8 | /// if a builder might be applied (some builders are configured to auto-apply 9 | /// only to packages that explicitly depend on them). 10 | /// 11 | /// This function checks the current project's pubspec to see if any of these 12 | /// conditions are met: 13 | /// - The current package is [packageName] 14 | /// - [packageName] is a dependency 15 | /// - [packageName] is a dev dependency 16 | /// - [packageName] is a dependency override 17 | bool packageIsImmediateDependency(String packageName, {String? path}) { 18 | final pubspec = cachedPubspec(path: path); 19 | return pubspec.name == packageName || 20 | pubspec.devDependencies.keys.any((d) => d == packageName) || 21 | pubspec.dependencies.keys.any((d) => d == packageName) || 22 | pubspec.dependencyOverrides.keys.any((d) => d == packageName); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/utils/parse_flag_from_args.dart: -------------------------------------------------------------------------------- 1 | bool parseFlagFromArgs(List args, String name, 2 | {String? abbr, bool defaultsTo = false, bool negatable = false}) { 3 | // Ignore all args after a separator. 4 | final argsBeforeSep = args.takeWhile((arg) => arg != '--').toList(); 5 | 6 | // Iterate in reverse so that in the event of multiple instances of the flag, 7 | // the last one wins. 8 | for (var i = argsBeforeSep.length - 1; i >= 0; i--) { 9 | // Return true if the flag is found. 10 | if (argsBeforeSep[i] == '--$name') { 11 | return true; 12 | } 13 | 14 | // Return true if the abbreviated flag is found. 15 | if (abbr != null && abbr.isNotEmpty && argsBeforeSep[i] == '-$abbr') { 16 | return true; 17 | } 18 | 19 | // Return false if the flag is negatable and the negated version is found. 20 | if (negatable && argsBeforeSep[i] == '--no-$name') { 21 | return false; 22 | } 23 | } 24 | 25 | return defaultsTo; 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/utils/parse_imports.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | /// Return the contents of the enquoted portion of the import statements in the 4 | /// file. Not 100% accurate, since we use regular expressions instead of the 5 | /// Dart AST to extract the imports. 6 | Iterable parseImports(String fileContents) => 7 | _importRegex.allMatches(fileContents).map((m) => m.group(1)).whereNotNull(); 8 | 9 | final _importRegex = 10 | RegExp(r'''^import ['"]([^'"]+)['"];?$''', multiLine: true); 11 | 12 | /// Return a set of package names given a list of imports. 13 | Set computePackageNamesFromImports(Iterable imports) => imports 14 | .map((i) => _packageNameFromImportRegex.matchAsPrefix(i)?.group(1)) 15 | .whereNotNull() 16 | .toSet(); 17 | 18 | final _packageNameFromImportRegex = RegExp(r'package:([^/]+)/.+'); 19 | -------------------------------------------------------------------------------- /lib/src/utils/process_declaration.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class ProcessDeclaration { 4 | final List args; 5 | final String executable; 6 | final ProcessStartMode? mode; 7 | final String? workingDirectory; 8 | 9 | ProcessDeclaration(this.executable, this.args, 10 | {this.mode, this.workingDirectory}); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/utils/pubspec_lock.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:yaml/yaml.dart'; 4 | 5 | /// Index into the pubspecLock to locate the 'source' field for the given 6 | /// package. 7 | String? _getPubSpecLockPackageSource( 8 | YamlDocument pubSpecLock, String packageName) { 9 | final contents = pubSpecLock.contents; 10 | if (contents is YamlMap) { 11 | final packages = contents['packages']; 12 | if (packages is YamlMap) { 13 | final specificDependency = packages[packageName]; 14 | if (specificDependency is YamlMap) return specificDependency['source']; 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | /// Return a mapping of package name to dependency 'type', using the pubspec 21 | /// lock document. If a package cannot be located in the pubspec lock document, 22 | /// it will map to null. 23 | HashMap getDependencySources( 24 | YamlDocument pubspecLockDocument, Iterable packageNames) => 25 | HashMap.fromIterable(packageNames, 26 | value: (name) => 27 | _getPubSpecLockPackageSource(pubspecLockDocument, name)); 28 | -------------------------------------------------------------------------------- /lib/src/utils/rest_args_with_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | 3 | /// Returns the "rest" args from [argResults], but with the arg separator "--" 4 | /// restored to its original position if it was included. 5 | /// 6 | /// This is necessary because [ArgResults.rest] will _not_ include the separator 7 | /// unless it stopped parsing before it reached the separator. 8 | /// 9 | /// The use case for this is a [CompoundTool] that uses the [takeAllArgs] arg 10 | /// mapper, because the goal there is to forward on the original args minus the 11 | /// consumed options and flags. If the separator has also been removed, you may 12 | /// hit an error when trying to parse those args. 13 | /// 14 | /// var parser = ArgParser()..addFlag('verbose', abbr: 'v'); 15 | /// var results = parser.parse(['a', '-v', 'b', '--', '--unknown', 'c']); 16 | /// print(results.rest); 17 | /// // ['a', 'b', '--unknown', 'c'] 18 | /// print(restArgsWithSeparator(results)); 19 | /// // ['a', 'b', '--', '--unknown', 'c'] 20 | List restArgsWithSeparator(ArgResults argResults) { 21 | // If no separator was used, return the rest args as is. 22 | if (!argResults.arguments.contains('--')) { 23 | return argResults.rest; 24 | } 25 | 26 | final args = argResults.arguments; 27 | final rest = argResults.rest; 28 | var restIndex = 0; 29 | for (var argsIndex = 0; argsIndex < args.length; argsIndex++) { 30 | // Iterate through the original args until we hit the first separator. 31 | if (args[argsIndex] == '--') break; 32 | // While doing so, move a cursor through the rest args list each time we 33 | // match up between the original list and the rest args list. This works 34 | // because the rest args list should be an ordered subset of the original 35 | // args list. 36 | if (args[argsIndex] == rest[restIndex]) { 37 | restIndex++; 38 | } 39 | } 40 | 41 | // At this point, [restIndex] should be pointing to the spot where the first 42 | // arg separator should be restored. 43 | return [...rest.sublist(0, restIndex), '--', ...rest.sublist(restIndex)]; 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/utils/run_process_and_ensure_exit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logging/logging.dart'; 4 | 5 | import 'ensure_process_exit.dart'; 6 | import 'process_declaration.dart'; 7 | 8 | Future runProcessAndEnsureExit(ProcessDeclaration processDeclaration, 9 | {Logger? log}) async { 10 | final process = await Process.start( 11 | processDeclaration.executable, processDeclaration.args, 12 | mode: processDeclaration.mode ?? ProcessStartMode.normal, 13 | workingDirectory: processDeclaration.workingDirectory); 14 | ensureProcessExit(process, log: log); 15 | return process.exitCode; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/utils/start_process_and_ensure_exit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logging/logging.dart'; 4 | 5 | import 'ensure_process_exit.dart'; 6 | import 'process_declaration.dart'; 7 | 8 | Future startProcessAndEnsureExit(ProcessDeclaration processDeclaration, 9 | {Logger? log}) async { 10 | final process = await Process.start( 11 | processDeclaration.executable, processDeclaration.args.toList(), 12 | mode: processDeclaration.mode ?? ProcessStartMode.normal, 13 | workingDirectory: processDeclaration.workingDirectory); 14 | ensureProcessExit(process, log: log); 15 | return process; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/utils/verbose_enabled.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/command_runner.dart'; 2 | 3 | bool verboseEnabled(Command command) => 4 | command.globalResults!['verbose'] ?? false; 5 | -------------------------------------------------------------------------------- /lib/src/utils/version.dart: -------------------------------------------------------------------------------- 1 | // The utility in this file was originally modeled after: 2 | // https://github.com/dart-lang/test/blob/1ccf56082adf35d5436e09793f547dbaa2e48218/pkgs/test_core/lib/src/runner/version.dart#L9-L60 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:yaml/yaml.dart'; 7 | 8 | /// The version number of the dart_dev package, or `null` if it couldn't be 9 | /// loaded. 10 | /// 11 | /// This is a semantic version, optionally followed by a space and additional 12 | /// data about its source. 13 | final String? dartDevVersion = (() { 14 | dynamic lockfile; 15 | try { 16 | lockfile = loadYaml(File('pubspec.lock').readAsStringSync()); 17 | } on FormatException catch (_) { 18 | return null; 19 | } on IOException catch (_) { 20 | return null; 21 | } 22 | 23 | if (lockfile is! Map) return null; 24 | var packages = lockfile['packages']; 25 | if (packages is! Map) return null; 26 | var package = packages['dart_dev']; 27 | if (package is! Map) return null; 28 | 29 | var source = package['source']; 30 | if (source is! String) return null; 31 | 32 | switch (source) { 33 | case 'hosted': 34 | var version = package['version']; 35 | return (version is String) ? version : null; 36 | 37 | case 'git': 38 | var version = package['version']; 39 | if (version is! String) return null; 40 | var description = package['description']; 41 | if (description is! Map) return null; 42 | var ref = description['resolved-ref']; 43 | if (ref is! String) return null; 44 | 45 | return '$version (${ref.substring(0, 7)})'; 46 | 47 | case 'path': 48 | var version = package['version']; 49 | if (version is! String) return null; 50 | var description = package['description']; 51 | if (description is! Map) return null; 52 | var path = description['path']; 53 | if (path is! String) return null; 54 | 55 | return '$version (from $path)'; 56 | 57 | default: 58 | return null; 59 | } 60 | })(); 61 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | export 'src/utils/arg_results_utils.dart'; 2 | export 'src/utils/assert_no_positional_args_nor_args_after_separator.dart'; 3 | export 'src/utils/cached_pubspec.dart'; 4 | export 'src/utils/ensure_process_exit.dart'; 5 | export 'src/utils/global_package_is_active_and_compatible.dart'; 6 | export 'src/utils/logging.dart' 7 | show humanReadable, logSubprocessHeader, logTimedAsync, logTimedSync; 8 | export 'src/utils/organize_directives/organize_directives.dart' 9 | show organizeDirectives; 10 | export 'src/utils/package_is_immediate_dependency.dart'; 11 | export 'src/utils/process_declaration.dart'; 12 | export 'src/utils/run_process_and_ensure_exit.dart'; 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev 2 | version: 4.2.3 3 | description: > 4 | Centralized tooling for Dart projects. 5 | Consistent interface across projects. 6 | Easily configurable. 7 | repository: https://github.com/Workiva/dart_dev 8 | 9 | environment: 10 | sdk: '>=2.19.0 <4.0.0' 11 | 12 | dependencies: 13 | analyzer: ">=5.0.0 <8.0.0" 14 | args: ^2.0.0 15 | async: ^2.5.0 16 | crypto: ^3.0.1 17 | glob: ^2.0.0 18 | io: ^1.0.0 19 | logging: ^1.0.0 20 | path: ^1.8.0 21 | pub_semver: ^2.0.0 22 | pubspec_parse: ^1.0.0 23 | stack_trace: ^1.10.0 24 | yaml: ^3.0.0 25 | collection: ^1.15.0 26 | 27 | dev_dependencies: 28 | build_runner: ^2.0.0 29 | dependency_validator: ">=4.0.0 <6.0.0" 30 | lints: ">=2.0.0 <7.0.0" 31 | matcher: ^0.12.5 32 | test: ^1.15.7 33 | test_descriptor: ^2.0.0 34 | test_process: ^2.0.0 35 | -------------------------------------------------------------------------------- /test/functional.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_dev/src/utils/executables.dart' as exe; 5 | import 'package:path/path.dart' as p; 6 | import 'package:test_descriptor/test_descriptor.dart' as d; 7 | import 'package:test_process/test_process.dart'; 8 | 9 | Future runDevToolFunctionalTest( 10 | String command, String projectTemplatePath, 11 | {List? args, bool? verbose}) async { 12 | // Setup a temporary directory on which this tool will be run, using the given 13 | // project template as a starting point. 14 | final templateDir = Directory(projectTemplatePath); 15 | if (!templateDir.existsSync()) { 16 | throw ArgumentError( 17 | 'projectTemplatePath does not exist: $projectTemplatePath'); 18 | } 19 | 20 | final templateFiles = templateDir 21 | .listSync(recursive: true) 22 | .whereType() 23 | .map((f) => f.absolute); 24 | for (final file in templateFiles) { 25 | final target = 26 | p.join(d.sandbox, p.relative(file.path, from: templateDir.path)); 27 | Directory(p.dirname(target)).createSync(recursive: true); 28 | file.copySync(target); 29 | } 30 | 31 | final pubspecs = Directory(d.sandbox) 32 | .listSync(recursive: true) 33 | .whereType() 34 | .where((f) => p.basename(f.path) == 'pubspec.yaml') 35 | .map((f) => f.absolute); 36 | final pathDepPattern = RegExp(r'path: (.*)'); 37 | 38 | for (final pubspec in pubspecs) { 39 | final updated = 40 | pubspec.readAsStringSync().replaceAllMapped(pathDepPattern, (match) { 41 | final relDepPath = match.group(1); 42 | final relPubspecPath = p.relative(pubspec.path, from: d.sandbox); 43 | var absPath = p.absolute(p.normalize( 44 | p.join(templateDir.path, relPubspecPath, relDepPath, '..'))); 45 | // Since pubspec paths should always be posix style or dart analyze 46 | // complains, switch to forward slashes on windows 47 | if (Platform.isWindows) { 48 | absPath = absPath.replaceAll('\\', '/'); 49 | } 50 | return 'path: $absPath'; 51 | }); 52 | pubspec.writeAsStringSync(updated); 53 | 54 | final result = Process.runSync(exe.dart, ['pub', 'get'], 55 | workingDirectory: pubspec.parent.path); 56 | if (result.exitCode != 0) { 57 | final origPath = p.join(p.relative(templateDir.absolute.path), 58 | p.relative(pubspec.absolute.path, from: d.sandbox)); 59 | throw StateError('dart pub get failed on: $origPath\n' 60 | 'STDOUT:\n${result.stdout}\n' 61 | 'STDERR:\n${result.stderr}\n'); 62 | } 63 | } 64 | 65 | final allArgs = ['run', 'dart_dev', command, ...?args]; 66 | return TestProcess.start(exe.dart, allArgs, workingDirectory: d.sandbox); 67 | } 68 | -------------------------------------------------------------------------------- /test/functional/analyze_tool_functional_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | @Timeout(Duration(seconds: 20)) 3 | import 'package:test/test.dart'; 4 | 5 | import '../functional.dart'; 6 | 7 | void main() { 8 | test('success', () async { 9 | final process = await runDevToolFunctionalTest( 10 | 'analyze', 'test/functional/fixtures/analyze/success'); 11 | await process.shouldExit(0); 12 | }); 13 | 14 | test('failure', () async { 15 | final process = await runDevToolFunctionalTest( 16 | 'analyze', 'test/functional/fixtures/analyze/failure'); 17 | await process.shouldExit(greaterThan(0)); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/functional/documentation_test.dart: -------------------------------------------------------------------------------- 1 | /// Scans the project for Dart code blocks in markdown files and runs static 2 | /// analysis on each to ensure that all of our documentation is valid. 3 | /// 4 | /// To opt out of this, add `test=false` to the opening code fence: 5 | /// 6 | /// ```dart test=false 7 | /// // Code that we know won't statically analyze... 8 | /// ``` 9 | @TestOn('vm') 10 | @Timeout(Duration(seconds: 10)) 11 | library test.functional.documentation_test; 12 | 13 | import 'dart:io'; 14 | 15 | import 'package:dart_dev/src/utils/executables.dart' as exe; 16 | import 'package:glob/glob.dart'; 17 | import 'package:glob/list_local_fs.dart'; 18 | import 'package:path/path.dart' as p; 19 | import 'package:test/test.dart'; 20 | import 'package:test_descriptor/test_descriptor.dart' as d; 21 | import 'package:test_process/test_process.dart'; 22 | 23 | void main() { 24 | for (final dartBlock in getDartBlocks()) { 25 | test('${dartBlock.sourceUrl} (block #${dartBlock.index})', () async { 26 | final pubspecSource = pubspecWithPackages(dartBlock.packages); 27 | await d.dir('project', [ 28 | d.file('doc.dart', dartBlock.source), 29 | d.file('pubspec.yaml', pubspecSource), 30 | ]).create(); 31 | 32 | final pubGet = await TestProcess.start(exe.dart, ['pub', 'get'], 33 | workingDirectory: '${d.sandbox}/project'); 34 | printOnFailure('PUBSPEC:\n$pubspecSource\n'); 35 | await pubGet.shouldExit(0); 36 | 37 | final analysis = await TestProcess.start( 38 | exe.dart, ['analyze', '--fatal-infos'], 39 | workingDirectory: '${d.sandbox}/project'); 40 | printOnFailure('SOURCE:\n${dartBlock.source}\n'); 41 | await analysis.shouldExit(0); 42 | }); 43 | } 44 | } 45 | 46 | Iterable getDartBlocks() sync* { 47 | final dartBlockPattern = 48 | RegExp(r'^```dart *([^\r\n]*)([^`]*)^```', multiLine: true); 49 | for (final file in Glob('**.md').listSync().whereType()) { 50 | final source = file.readAsStringSync(); 51 | var i = 1; 52 | for (final match in dartBlockPattern.allMatches(source)) { 53 | final params = match.group(1)!.split(' '); 54 | if (params.contains('test=false')) continue; 55 | yield DartBlock.fromSource(match.group(1)!, file.path, i++); 56 | } 57 | } 58 | } 59 | 60 | String pubspecWithPackages(Set packages) { 61 | final buffer = StringBuffer() 62 | ..writeln('name: doc_test') 63 | ..writeln('environment:') 64 | ..writeln(' sdk: ">=2.12.0 <3.0.0"') 65 | ..writeln('dependencies:'); 66 | for (final package in packages) { 67 | var constraint = 68 | package == 'dart_dev' ? '\n path: ${p.current}' : ' any'; 69 | buffer.writeln(' $package:$constraint'); 70 | } 71 | return buffer.toString(); 72 | } 73 | 74 | class DartBlock { 75 | final int index; 76 | final Set packages; 77 | final String source; 78 | final String sourceUrl; 79 | 80 | DartBlock.fromSource(this.source, this.sourceUrl, this.index) 81 | : packages = parsePackagesFromSource(source); 82 | 83 | static Set parsePackagesFromSource(String source) { 84 | final packagePattern = RegExp(r'''['"]package:(\w+)\/.*['"]'''); 85 | return { 86 | for (final match in packagePattern.allMatches(source)) 87 | if (match.group(1) != null) match.group(1)! 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/functional/fixtures/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # By default, when an analysis context is established, it will look for 2 | # configuration in an analysis_options.yaml file, starting in the current 3 | # directory and working up through parent directories until it finds one. 4 | 5 | # The analysis_options.yaml at the root of this project excludes the 6 | # test_fixtures/ directory because it intentionally contains code that would 7 | # otherwise get flagged by the analyzer as lints/warnings. 8 | 9 | # However, if the test_fixtures/ directory is excluded when trying to use the 10 | # analysis APIs on code in that directory (which is what our tests do), they 11 | # will fail. 12 | 13 | # This file works around that problem because the analysis context will use this 14 | # (empty) configuration instead, which does not exclude the test fixtures. 15 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/failure/lib/lib.dart: -------------------------------------------------------------------------------- 1 | final isValidDart = FALSE; 2 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/failure/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_analyze_failure 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../.. 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/failure/tool/dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | 3 | final config = { 4 | 'analyze': AnalyzeTool(), 5 | }; 6 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/success/lib/lib.dart: -------------------------------------------------------------------------------- 1 | final isValidDart = true; 2 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/success/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_analyze_success 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../.. 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/analyze/success/tool/dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | 3 | final config = { 4 | 'analyze': AnalyzeTool(), 5 | }; 6 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_off/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'dart:async'; 3 | 4 | void doStuff({TestFailure t}) async { 5 | await Future.delayed(Duration(seconds: 1)); 6 | print(t.message); 7 | } 8 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_off/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_format_organize_directives_off 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../../../ 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_off/tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | import 'package:glob/glob.dart'; 3 | 4 | final config = { 5 | 'format': FormatTool() 6 | ..formatter = Formatter.dartFormat 7 | ..organizeDirectives = false 8 | ..exclude = [Glob('tool/**')], 9 | }; 10 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_on/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'dart:async'; 3 | 4 | void doStuff({TestFailure t}) async { 5 | await Future.delayed(Duration(seconds: 1)); 6 | print(t.message); 7 | } 8 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_on/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_format_organize_directives_on 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../../../ 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/format/unsorted_imports/organize_directives_on/tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | import 'package:glob/glob.dart'; 3 | 4 | final config = { 5 | 'format': FormatTool() 6 | ..formatter = Formatter.dartFormat 7 | ..organizeDirectives = true 8 | ..exclude = [Glob('tool/**')], 9 | }; 10 | -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config/lib/lib.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/functional/fixtures/null_safety/opted_in_custom_config/lib/lib.dart -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_null_safety_opted_in_custom_config_version_comment 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../.. 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config/tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | 3 | final config = { 4 | ...coreConfig, 5 | }; 6 | -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config_version_comment/lib/lib.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/functional/fixtures/null_safety/opted_in_custom_config_version_comment/lib/lib.dart -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config_version_comment/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_null_safety_opted_in_custom_config 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../.. 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_custom_config_version_comment/tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | // @dart=2.9 2 | import 'package:dart_dev/dart_dev.dart'; 3 | 4 | final config = { 5 | ...coreConfig, 6 | }; 7 | -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_no_config/lib/lib.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/functional/fixtures/null_safety/opted_in_no_config/lib/lib.dart -------------------------------------------------------------------------------- /test/functional/fixtures/null_safety/opted_in_no_config/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_dev_test_functional_null_safety_opted_in_no_config 2 | version: 0.0.0 3 | environment: 4 | sdk: ">=2.12.0" 5 | dev_dependencies: 6 | dart_dev: 7 | path: ../../../../.. 8 | 9 | workiva: 10 | disable_core_checks: true 11 | -------------------------------------------------------------------------------- /test/functional/format_tool_functional_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | @Timeout(Duration(seconds: 20)) 3 | import 'dart:io'; 4 | 5 | import 'package:test/test.dart'; 6 | import 'package:test_descriptor/test_descriptor.dart' as d; 7 | 8 | import '../functional.dart'; 9 | 10 | void main() { 11 | group('Format Tool', () { 12 | Future<_SourceFile> format(String projectPath) async { 13 | const filePath = 'lib/main.dart'; 14 | 15 | final process = await runDevToolFunctionalTest('format', projectPath); 16 | await process.shouldExit(0); 17 | 18 | final contentsBefore = File('$projectPath$filePath').readAsStringSync(); 19 | final contentsAfter = File('${d.sandbox}/$filePath').readAsStringSync(); 20 | return _SourceFile(contentsBefore, contentsAfter); 21 | } 22 | 23 | test('organize directives off', () async { 24 | const projectPath = 25 | 'test/functional/fixtures/format/unsorted_imports/organize_directives_off/'; 26 | final sourceFile = await format(projectPath); 27 | expect( 28 | sourceFile.contentsBefore, 29 | equals(sourceFile.contentsAfter), 30 | ); 31 | }); 32 | 33 | test('organize directives on', () async { 34 | const projectPath = 35 | 'test/functional/fixtures/format/unsorted_imports/organize_directives_on/'; 36 | final sourceFile = await format(projectPath); 37 | expect( 38 | sourceFile.contentsBefore, 39 | isNot(equals(sourceFile.contentsAfter)), 40 | ); 41 | }); 42 | }); 43 | } 44 | 45 | class _SourceFile { 46 | String contentsBefore; 47 | 48 | String contentsAfter; 49 | 50 | _SourceFile(this.contentsBefore, this.contentsAfter); 51 | } 52 | -------------------------------------------------------------------------------- /test/functional/null_safety_functional_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | @Timeout(Duration(seconds: 20)) 3 | import 'package:test/test.dart'; 4 | 5 | import '../functional.dart'; 6 | 7 | void main() { 8 | group('runs properly in a project that has opted into null safety', () { 9 | test('without any custom config', () async { 10 | final process = await runDevToolFunctionalTest( 11 | 'analyze', 'test/functional/fixtures/null_safety/opted_in_no_config'); 12 | await process.shouldExit(0); 13 | }); 14 | 15 | test('with a custom config', () async { 16 | final process = await runDevToolFunctionalTest('analyze', 17 | 'test/functional/fixtures/null_safety/opted_in_custom_config'); 18 | await process.shouldExit(0); 19 | }); 20 | 21 | test('with a custom config that has a language version comment', () async { 22 | final process = await runDevToolFunctionalTest('analyze', 23 | 'test/functional/fixtures/null_safety/opted_in_custom_config_version_comment'); 24 | await process.shouldExit(0); 25 | }, tags: 'dart2'); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/log_matchers.dart: -------------------------------------------------------------------------------- 1 | // These matchers are borrowed from the build package: 2 | // https://github.com/dart-lang/build/blob/a337a908a25e4d1bd06e898f40c3c013a7ec04e3/build_test/lib/src/record_logs.dart 3 | library dart_dev.test.log_matchers; 4 | 5 | import 'package:logging/logging.dart'; 6 | import 'package:matcher/matcher.dart'; 7 | 8 | /// Matches [LogRecord] of any level whose message is [messageOrMatcher]. 9 | /// 10 | /// ```dart 11 | /// anyLogOf('Hello World)'; // Exactly match 'Hello World'. 12 | /// anyLogOf(contains('ERROR')); // Contains the sub-string 'ERROR'. 13 | /// ``` 14 | Matcher anyLogOf(dynamic messageOrMatcher) => 15 | _LogRecordMatcher(anything, messageOrMatcher); 16 | 17 | /// Matches [LogRecord] of [Level.FINE] where message is [messageOrMatcher]. 18 | Matcher fineLogOf(dynamic messageOrMatcher) => 19 | _LogRecordMatcher(Level.FINE, messageOrMatcher); 20 | 21 | /// Matches [LogRecord] of [Level.INFO] where message is [messageOrMatcher]. 22 | Matcher infoLogOf(dynamic messageOrMatcher) => 23 | _LogRecordMatcher(Level.INFO, messageOrMatcher); 24 | 25 | /// Matches [LogRecord] of [Level.WARNING] where message is [messageOrMatcher]. 26 | Matcher warningLogOf(dynamic messageOrMatcher) => 27 | _LogRecordMatcher(Level.WARNING, messageOrMatcher); 28 | 29 | /// Matches [LogRecord] of [Level.SEVERE] where message is [messageOrMatcher]. 30 | Matcher severeLogOf(dynamic messageOrMatcher) => 31 | _LogRecordMatcher(Level.SEVERE, messageOrMatcher); 32 | 33 | class _LogRecordMatcher extends Matcher { 34 | final Matcher _level; 35 | final Matcher _message; 36 | 37 | factory _LogRecordMatcher(dynamic levelOr, dynamic messageOr) => 38 | _LogRecordMatcher._(levelOr is Matcher ? levelOr : equals(levelOr), 39 | messageOr is Matcher ? messageOr : equals(messageOr)); 40 | 41 | _LogRecordMatcher._(this._level, this._message); 42 | 43 | @override 44 | Description describe(Description description) { 45 | description.add('level: '); 46 | _level.describe(description); 47 | description.add(', message: '); 48 | _message.describe(description); 49 | return description; 50 | } 51 | 52 | @override 53 | Description describeMismatch(covariant LogRecord item, 54 | Description description, Map _, bool __) { 55 | if (!_level.matches(item.level, {})) { 56 | _level.describeMismatch(item.level, description, {}, false); 57 | } 58 | if (!_message.matches(item.message, {})) { 59 | _message.describeMismatch(item.message, description, {}, false); 60 | } 61 | return description; 62 | } 63 | 64 | @override 65 | bool matches(dynamic item, Map _) => 66 | item is LogRecord && 67 | _level.matches(item.level, {}) && 68 | _message.matches(item.message, {}); 69 | } 70 | -------------------------------------------------------------------------------- /test/tools/analyze_tool_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'package:args/args.dart'; 3 | import 'package:args/command_runner.dart'; 4 | import 'package:dart_dev/src/dart_dev_tool.dart'; 5 | import 'package:dart_dev/src/tools/analyze_tool.dart'; 6 | import 'package:dart_dev/src/utils/executables.dart' as exe; 7 | import 'package:glob/glob.dart'; 8 | import 'package:logging/logging.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import '../log_matchers.dart'; 12 | import 'shared_tool_tests.dart'; 13 | 14 | void main() { 15 | final globRoot = 'test/tools/fixtures/analyze/globs/'; 16 | 17 | group('AnalyzeTool', () { 18 | sharedDevToolTests(() => AnalyzeTool()); 19 | 20 | test('provides an argParser', () { 21 | final argParser = AnalyzeTool().argParser; 22 | expect(argParser.options, contains('analyzer-args')); 23 | expect(argParser.options['analyzer-args']!.type, OptionType.single); 24 | }); 25 | }); 26 | 27 | group('buildArgs', () { 28 | test('defaults to an empty list', () { 29 | expect(buildArgs(), isEmpty); 30 | }); 31 | 32 | test('combines configured args and cli args (in that order)', () { 33 | final argParser = AnalyzeTool().toCommand('t').argParser; 34 | final argResults = argParser.parse(['--analyzer-args', 'c d']); 35 | expect( 36 | buildArgs(argResults: argResults, configuredAnalyzerArgs: ['a', 'b']), 37 | orderedEquals(['a', 'b', 'c', 'd'])); 38 | }); 39 | 40 | test( 41 | 'combines configured args and cli args (in that order) with useDartAnalyze', 42 | () { 43 | final argParser = AnalyzeTool().toCommand('t').argParser; 44 | final argResults = argParser.parse(['--analyzer-args', 'c d']); 45 | expect( 46 | buildArgs( 47 | argResults: argResults, 48 | configuredAnalyzerArgs: ['a', 'b'], 49 | useDartAnalyze: true), 50 | orderedEquals(['analyze', 'a', 'b', 'c', 'd'])); 51 | }); 52 | 53 | test('inserts a verbose flag if not already present', () { 54 | final argParser = AnalyzeTool().toCommand('t').argParser; 55 | final argResults = argParser.parse(['--analyzer-args', 'c d']); 56 | expect( 57 | buildArgs( 58 | argResults: argResults, 59 | configuredAnalyzerArgs: ['a', 'b'], 60 | verbose: true), 61 | orderedEquals(['a', 'b', 'c', 'd', '-v'])); 62 | }); 63 | 64 | test('does not insert a duplicate verbose flag (-v)', () { 65 | expect(buildArgs(configuredAnalyzerArgs: ['-v'], verbose: true), 66 | orderedEquals(['-v'])); 67 | }); 68 | 69 | test('does not insert a duplicate verbose flag (--verbose)', () { 70 | expect(buildArgs(configuredAnalyzerArgs: ['--verbose'], verbose: true), 71 | orderedEquals(['--verbose'])); 72 | }); 73 | }); 74 | 75 | group('buildEntrypoints', () { 76 | test('defaults to `.`', () { 77 | expect(buildEntrypoints(root: globRoot), ['.']); 78 | expect(buildEntrypoints(include: [], root: globRoot), ['.']); 79 | }); 80 | 81 | test('from one glob', () { 82 | expect(buildEntrypoints(include: [Glob('*.dart')], root: globRoot), 83 | ['${globRoot}file.dart']); 84 | }); 85 | 86 | test('from multiple globs', () { 87 | expect( 88 | buildEntrypoints( 89 | include: [Glob('*.dart'), Glob('*.txt')], root: globRoot), 90 | unorderedEquals(['${globRoot}file.dart', '${globRoot}file.txt'])); 91 | }); 92 | }); 93 | 94 | group('buildProcess', () { 95 | test('throws UsageException if positional args are given', () { 96 | final argResults = ArgParser().parse(['a']); 97 | final context = DevToolExecutionContext( 98 | argResults: argResults, commandName: 'test_analyze'); 99 | expect( 100 | () => buildProcess(context), 101 | throwsA(isA() 102 | .having( 103 | (e) => e.message, 'command name', contains('test_analyze')) 104 | .having((e) => e.message, 'usage footer', 105 | contains('--analyzer-args')))); 106 | }); 107 | 108 | test('throws UsageException if args are given after a separator', () { 109 | final argResults = ArgParser().parse(['--', 'a']); 110 | final context = DevToolExecutionContext( 111 | argResults: argResults, commandName: 'test_analyze'); 112 | expect( 113 | () => buildProcess(context), 114 | throwsA(isA() 115 | .having( 116 | (e) => e.message, 'command name', contains('test_analyze')) 117 | .having((e) => e.message, 'usage footer', 118 | contains('--analyzer-args')))); 119 | }); 120 | 121 | test('returns a ProcessDeclaration (default)', () { 122 | final context = DevToolExecutionContext(); 123 | final process = buildProcess(context); 124 | expect(process.executable, exe.dartanalyzer); 125 | expect(process.args, orderedEquals(['.'])); 126 | }); 127 | 128 | test('returns a ProcessDeclaration with useDartAnalyze (default)', () { 129 | final context = DevToolExecutionContext(); 130 | final process = buildProcess(context, useDartAnalyze: true); 131 | expect(process.executable, exe.dart); 132 | expect(process.args, orderedEquals(['analyze', '.'])); 133 | }); 134 | 135 | test('returns a ProcessDeclaration (with args)', () { 136 | final argParser = AnalyzeTool().toCommand('t').argParser; 137 | final argResults = 138 | argParser.parse(['--analyzer-args', '--dart-sdk /sdk']); 139 | final context = DevToolExecutionContext(argResults: argResults); 140 | final process = buildProcess(context, 141 | configuredAnalyzerArgs: ['--fatal-infos', '--fatal-warnings'], 142 | include: [Glob('*.dart'), Glob('*.txt')], 143 | path: globRoot); 144 | expect(process.executable, exe.dartanalyzer); 145 | expect( 146 | process.args, 147 | orderedEquals([ 148 | '--fatal-infos', 149 | '--fatal-warnings', 150 | '--dart-sdk', 151 | '/sdk', 152 | '${globRoot}file.dart', 153 | '${globRoot}file.txt', 154 | ])); 155 | }); 156 | 157 | test('returns a ProcessDeclaration with useDartAnalyzer (with args)', () { 158 | final argParser = AnalyzeTool().toCommand('t').argParser; 159 | final argResults = 160 | argParser.parse(['--analyzer-args', '--dart-sdk /sdk']); 161 | final context = DevToolExecutionContext(argResults: argResults); 162 | final process = buildProcess(context, 163 | configuredAnalyzerArgs: ['--fatal-infos', '--fatal-warnings'], 164 | include: [Glob('*.dart')], 165 | path: globRoot, 166 | useDartAnalyze: true); 167 | expect(process.executable, exe.dart); 168 | expect( 169 | process.args, 170 | orderedEquals([ 171 | 'analyze', 172 | '--fatal-infos', 173 | '--fatal-warnings', 174 | '--dart-sdk', 175 | '/sdk', 176 | '${globRoot}file.dart', 177 | ])); 178 | }); 179 | 180 | test('returns a ProcessDeclaration (verbose)', () { 181 | final argParser = AnalyzeTool().toCommand('t').argParser; 182 | final argResults = 183 | argParser.parse(['--analyzer-args', '--dart-sdk /sdk']); 184 | final context = 185 | DevToolExecutionContext(argResults: argResults, verbose: true); 186 | final process = buildProcess(context, 187 | configuredAnalyzerArgs: ['--fatal-infos', '--fatal-warnings'], 188 | include: [Glob('*.dart'), Glob('*.txt')], 189 | path: globRoot); 190 | expect(process.executable, exe.dartanalyzer); 191 | expect( 192 | process.args, 193 | orderedEquals([ 194 | '--fatal-infos', 195 | '--fatal-warnings', 196 | '--dart-sdk', 197 | '/sdk', 198 | '-v', 199 | '${globRoot}file.dart', 200 | '${globRoot}file.txt', 201 | ])); 202 | }); 203 | }); 204 | 205 | group('logCommand', () { 206 | test('with <=5 entrypoints', () { 207 | expect(Logger.root.onRecord, 208 | emitsThrough(infoLogOf(contains('dartanalyzer -t a b c d e')))); 209 | logCommand(['-t'], ['a', 'b', 'c', 'd', 'e'], useDartAnalyzer: false); 210 | }); 211 | 212 | test('with >5 entrypoints', () { 213 | expect(Logger.root.onRecord, 214 | emitsThrough(infoLogOf(contains('dartanalyzer -t <6 paths>')))); 215 | logCommand(['-t'], ['a', 'b', 'c', 'd', 'e', 'f']); 216 | }); 217 | 218 | test('with >5 entrypoints in verbose mode', () { 219 | expect(Logger.root.onRecord, 220 | emitsThrough(infoLogOf(contains('dartanalyzer -t a b c d e f')))); 221 | logCommand(['-t'], ['a', 'b', 'c', 'd', 'e', 'f'], verbose: true); 222 | }); 223 | 224 | test('in useDartAnalyze mode', () { 225 | expect(Logger.root.onRecord, 226 | emitsThrough(infoLogOf(contains('dart analyze -t a')))); 227 | logCommand(['analyze', '-t'], ['a'], useDartAnalyzer: true); 228 | }); 229 | }); 230 | } 231 | -------------------------------------------------------------------------------- /test/tools/fixtures/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # By default, when an analysis context is established, it will look for 2 | # configuration in an analysis_options.yaml file, starting in the current 3 | # directory and working up through parent directories until it finds one. 4 | 5 | # The analysis_options.yaml at the root of this project excludes the 6 | # test_fixtures/ directory because it intentionally contains code that would 7 | # otherwise get flagged by the analyzer as lints/warnings. 8 | 9 | # However, if the test_fixtures/ directory is excluded when trying to use the 10 | # analysis APIs on code in that directory (which is what our tests do), they 11 | # will fail. 12 | 13 | # This file works around that problem because the analysis context will use this 14 | # (empty) configuration instead, which does not exclude the test fixtures. 15 | -------------------------------------------------------------------------------- /test/tools/fixtures/analyze/failing/failing.dart: -------------------------------------------------------------------------------- 1 | not valid dart -------------------------------------------------------------------------------- /test/tools/fixtures/analyze/globs/file.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/analyze/globs/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/tools/fixtures/analyze/globs/file.txt -------------------------------------------------------------------------------- /test/tools/fixtures/analyze/passing/passing.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/.dart_tool_test/file.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/tools/fixtures/format/globs/.dart_tool_test/file.dart -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/file.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/tools/fixtures/format/globs/file.txt -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/lib/sub/file.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/linked.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/tools/fixtures/format/globs/linked.dart -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/links/lib-link: -------------------------------------------------------------------------------- 1 | ../lib/ -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/links/link.dart: -------------------------------------------------------------------------------- 1 | ../linked.dart -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/links/not_link.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/other/file.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/dart_dev/2cb1dd108c883ca0f64136104ccb6bf3d8466804/test/tools/fixtures/format/globs/other/file.dart -------------------------------------------------------------------------------- /test/tools/fixtures/format/globs/should_exclude.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/has_dart_style/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: has_dart_style 2 | environment: 3 | sdk: ">=2.12.0 <3.0.0" 4 | dev_dependencies: 5 | dart_style: ^2.0.0 6 | 7 | workiva: 8 | disable_core_checks: true 9 | -------------------------------------------------------------------------------- /test/tools/fixtures/format/missing_dart_style/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: missing_dart_style 2 | environment: 3 | sdk: ">=2.12.0 <3.0.0" 4 | dev_dependencies: 5 | test: any 6 | 7 | workiva: 8 | disable_core_checks: true 9 | -------------------------------------------------------------------------------- /test/tools/fixtures/tuneup_check/has_tuneup/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: has_tuneup 2 | environment: 3 | sdk: ">=2.12.0 <3.0.0" 4 | dev_dependencies: 5 | tuneup: any 6 | 7 | workiva: 8 | disable_core_checks: true 9 | -------------------------------------------------------------------------------- /test/tools/fixtures/tuneup_check/missing_tuneup/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: missing_tuneup 2 | environment: 3 | sdk: ">=2.12.0 <3.0.0" 4 | 5 | workiva: 6 | disable_core_checks: true 7 | -------------------------------------------------------------------------------- /test/tools/function_tool_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'package:args/args.dart'; 3 | import 'package:args/command_runner.dart'; 4 | import 'package:dart_dev/dart_dev.dart'; 5 | import 'package:logging/logging.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../log_matchers.dart'; 9 | import 'shared_tool_tests.dart'; 10 | 11 | void main() { 12 | group('FunctionTool', () { 13 | sharedDevToolTests(() => DevTool.fromFunction((_) => 0)); 14 | 15 | test('forwards the returned exit code', () async { 16 | final tool = DevTool.fromFunction((_) => 1); 17 | expect(await tool.run(), 1); 18 | }); 19 | 20 | test('throws UsageException when no ArgParser given but args are present', 21 | () { 22 | final tool = DevTool.fromFunction((_) => 0); 23 | expect( 24 | () => tool.run( 25 | DevToolExecutionContext(argResults: ArgParser().parse(['foo']))), 26 | throwsA(isA())); 27 | }); 28 | 29 | test('accepts a custom ArgParser', () async { 30 | final parser = ArgParser()..addFlag('flag'); 31 | final tool = DevTool.fromFunction((context) { 32 | expect(context.argResults!['flag'], isTrue); 33 | return 0; 34 | }, argParser: parser); 35 | await tool 36 | .run(DevToolExecutionContext(argResults: parser.parse(['--flag']))); 37 | }); 38 | 39 | test('allows a custom ArgParser and args after a separator', () async { 40 | final tool = DevTool.fromFunction((_) => 0, argParser: ArgParser()); 41 | await tool.run(DevToolExecutionContext( 42 | argResults: ArgParser().parse(['--', 'foo']))); 43 | }); 44 | 45 | test('logs a warning if no exit code is returned', () { 46 | expect(Logger.root.onRecord, 47 | emitsThrough(warningLogOf(contains('did not return an exit code')))); 48 | DevTool.fromFunction((_) => null).run(); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/tools/process_tool_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'dart:async'; 3 | import 'dart:convert'; 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:args/command_runner.dart'; 7 | import 'package:dart_dev/dart_dev.dart'; 8 | import 'package:logging/logging.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import '../log_matchers.dart'; 12 | import 'shared_tool_tests.dart'; 13 | 14 | void main() { 15 | group('ProcessTool', () { 16 | sharedDevToolTests(() => DevTool.fromProcess('true', [])); 17 | 18 | test('forwards the returned exit code', () async { 19 | final tool = DevTool.fromProcess('false', []); 20 | expect(await tool.run(), isNonZero); 21 | }); 22 | 23 | test('can run from a custom working directory', () async { 24 | final tool = DevTool.fromProcess('pwd', [], workingDirectory: 'lib') 25 | as ProcessTool; 26 | expect(await tool.run(), isZero); 27 | final stdout = 28 | (await tool.process!.stdout.transform(utf8.decoder).join('')).trim(); 29 | expect(stdout, endsWith('/dart_dev/lib')); 30 | }); 31 | 32 | test('throws UsageException when args are present', () { 33 | final tool = DevTool.fromProcess('true', []); 34 | expect( 35 | () => tool.run( 36 | DevToolExecutionContext(argResults: ArgParser().parse(['foo']))), 37 | throwsA(isA())); 38 | }); 39 | 40 | test('logs the subprocess', () { 41 | expect(Logger.root.onRecord, 42 | emitsThrough(infoLogOf(contains('true foo bar')))); 43 | DevTool.fromProcess('true', ['foo', 'bar']).run(); 44 | }); 45 | }); 46 | 47 | group('BackgroundProcessTool', () { 48 | sharedDevToolTests(() => BackgroundProcessTool('true', []).starter); 49 | sharedDevToolTests(() => BackgroundProcessTool('true', []).stopper); 50 | 51 | test('starter runs the process without waiting for it to complete', 52 | () async { 53 | var processHasExited = false; 54 | final tool = BackgroundProcessTool('sleep', ['5']); 55 | expect(await tool.starter.run(), isZero); 56 | unawaited(tool.process!.exitCode.then((_) => processHasExited = true)); 57 | await Future.delayed(Duration.zero); 58 | expect(processHasExited, isFalse); 59 | await tool.stopper.run(); 60 | }); 61 | 62 | test('stopper stops the process immediately', () async { 63 | var processHasExited = false; 64 | final tool = BackgroundProcessTool('sleep', ['5']); 65 | final stopwatch = Stopwatch()..start(); 66 | expect(await tool.starter.run(), isZero); 67 | unawaited(tool.process!.exitCode.then((_) => processHasExited = true)); 68 | await Future.delayed(Duration(seconds: 1)); 69 | expect(processHasExited, isFalse); 70 | expect(await tool.stopper.run(), isZero); 71 | expect(processHasExited, isTrue); 72 | expect((stopwatch..stop()).elapsed.inSeconds, lessThan(3)); 73 | }); 74 | 75 | test('starter forwards the returned exit code', () async { 76 | final tool = BackgroundProcessTool('false', [], 77 | delayAfterStart: Duration(milliseconds: 500)); 78 | expect(await tool.starter.run(), isNonZero); 79 | }); 80 | 81 | test('stopper always returns a zero exit code', () async { 82 | final tool = BackgroundProcessTool('false', []); 83 | await tool.starter.run(); 84 | await Future.delayed(Duration(milliseconds: 500)); 85 | expect(await tool.stopper.run(), isZero); 86 | }); 87 | 88 | test('can run from a custom working directory', () async { 89 | final tool = BackgroundProcessTool('pwd', [], 90 | workingDirectory: 'lib', delayAfterStart: Duration(seconds: 1)); 91 | expect(await tool.starter.run(), isZero); 92 | final stdout = 93 | (await tool.process!.stdout.transform(utf8.decoder).join('')).trim(); 94 | expect(stdout, endsWith('/dart_dev/lib')); 95 | }); 96 | 97 | test('throws UsageException when args are present', () { 98 | final tool = BackgroundProcessTool('true', []); 99 | expect( 100 | () => tool.starter.run( 101 | DevToolExecutionContext(argResults: ArgParser().parse(['foo']))), 102 | throwsA(isA())); 103 | }); 104 | 105 | test('logs the subprocess', () { 106 | expect(Logger.root.onRecord, 107 | emitsThrough(infoLogOf(contains('true foo bar')))); 108 | BackgroundProcessTool('true', ['foo', 'bar']).starter.run(); 109 | }); 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /test/tools/shared_tool_tests.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/src/dart_dev_tool.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void sharedDevToolTests(DevTool Function() factory) { 5 | group('toCommand', () { 6 | test('should return a command with the given name', () { 7 | expect(factory().toCommand('custom_command').name, 'custom_command'); 8 | }); 9 | 10 | test('should return a command with a customizable description', () { 11 | expect((factory()..description = 'desc').toCommand('test').description, 12 | 'desc'); 13 | }); 14 | 15 | test('should return a command with a customizable hidden value', () { 16 | expect((factory()..hidden = false).toCommand('test').hidden, isFalse); 17 | expect((factory()..hidden = true).toCommand('test').hidden, isTrue); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/tools/tuneup_check_tool_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:args/command_runner.dart'; 6 | import 'package:dart_dev/dart_dev.dart'; 7 | import 'package:dart_dev/src/tools/tuneup_check_tool.dart'; 8 | import 'package:dart_dev/src/utils/executables.dart' as exe; 9 | import 'package:io/io.dart'; 10 | import 'package:logging/logging.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | import '../log_matchers.dart'; 14 | import 'shared_tool_tests.dart'; 15 | 16 | void main() { 17 | group('TuneupCheckTool', () { 18 | sharedDevToolTests(() => TuneupCheckTool()); 19 | 20 | test('provides an argParser', () { 21 | final argParser = TuneupCheckTool().argParser; 22 | expect(argParser.options, contains('ignore-infos')); 23 | expect(argParser.options['ignore-infos']!.type, OptionType.flag); 24 | }); 25 | }); 26 | 27 | group('buildArgs', () { 28 | test('(default)', () { 29 | expect(buildArgs(), orderedEquals(['run', 'tuneup', 'check'])); 30 | }); 31 | 32 | test('configured ignoreInfos', () { 33 | expect(buildArgs(configuredIgnoreInfos: true), 34 | orderedEquals(['run', 'tuneup', 'check', '--ignore-infos'])); 35 | }); 36 | 37 | test('--ignore-infos', () { 38 | final argResults = TuneupCheckTool().argParser.parse(['--ignore-infos']); 39 | expect(buildArgs(argResults: argResults), 40 | orderedEquals(['run', 'tuneup', 'check', '--ignore-infos'])); 41 | }); 42 | 43 | test('verbose', () { 44 | expect(buildArgs(verbose: true), 45 | orderedEquals(['run', 'tuneup', 'check', '--verbose'])); 46 | }); 47 | }); 48 | 49 | group('buildExecution', () { 50 | test('throws UsageException if positional args are given', () { 51 | final argResults = ArgParser().parse(['a']); 52 | final context = DevToolExecutionContext( 53 | argResults: argResults, commandName: 'test_tuneup'); 54 | expect( 55 | () => buildExecution(context), 56 | throwsA(isA().having( 57 | (e) => e.message, 'command name', contains('test_tuneup')))); 58 | }); 59 | 60 | test('exits early and logs if tuneup is not an immediate dependency', () { 61 | expect( 62 | Logger.root.onRecord, 63 | emitsThrough(severeLogOf(allOf(contains('Cannot run "tuneup check"'), 64 | contains('"tuneup" in pubspec.yaml'))))); 65 | 66 | final context = DevToolExecutionContext(); 67 | final execution = buildExecution(context, 68 | path: 'test/tools/fixtures/tuneup_check/missing_tuneup'); 69 | expect(execution.exitCode, ExitCode.config.code); 70 | }); 71 | 72 | group('returns a TuneupExecution', () { 73 | final path = 'test/tools/fixtures/tuneup_check/has_tuneup'; 74 | test('(default)', () { 75 | final execution = buildExecution(DevToolExecutionContext(), path: path); 76 | expect(execution.exitCode, isNull); 77 | expect(execution.process!.executable, exe.dart); 78 | expect( 79 | execution.process!.args, orderedEquals(['run', 'tuneup', 'check'])); 80 | expect(execution.process!.mode, ProcessStartMode.inheritStdio); 81 | }); 82 | 83 | test('with args', () { 84 | final argResults = 85 | TuneupCheckTool().argParser.parse(['--ignore-infos']); 86 | final context = 87 | DevToolExecutionContext(argResults: argResults, verbose: true); 88 | final execution = buildExecution(context, path: path); 89 | expect(execution.exitCode, isNull); 90 | expect(execution.process!.executable, exe.dart); 91 | expect( 92 | execution.process!.args, 93 | orderedEquals( 94 | ['run', 'tuneup', 'check', '--ignore-infos', '--verbose'])); 95 | expect(execution.process!.mode, ProcessStartMode.inheritStdio); 96 | }); 97 | 98 | test('and logs the subprocess header', () { 99 | expect(Logger.root.onRecord, 100 | emitsThrough(infoLogOf(allOf(contains('dart run tuneup check'))))); 101 | 102 | buildExecution(DevToolExecutionContext(), path: path); 103 | }); 104 | }); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_dev/src/utils/executables.dart' as exe; 5 | import 'package:dart_dev/utils.dart'; 6 | import 'package:pub_semver/pub_semver.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | class TempPubCache { 10 | final dir = Directory.systemTemp.createTempSync('test_pub_cache_'); 11 | Map get envOverride => {'PUB_CACHE': dir.absolute.path}; 12 | void tearDown() => dir.deleteSync(recursive: true); 13 | } 14 | 15 | /// Globally activates the given [package]. 16 | /// 17 | /// If non-null, [environment] will be passed to the [Process]. This provides a 18 | /// way to override certain things like the `PUB_CACHE` var that points pub to 19 | /// the global pub-cache directory. 20 | void globalActivate(String packageName, String constraint, 21 | {Map? environment}) { 22 | final result = Process.runSync( 23 | exe.dart, 24 | ['pub', 'global', 'activate', packageName, constraint], 25 | environment: environment, 26 | stderrEncoding: utf8, 27 | stdoutEncoding: utf8, 28 | ); 29 | if (result.exitCode != 0) { 30 | fail('Failed to global activate $packageName.\n' 31 | 'Process stdout:\n' 32 | '---------------\n' 33 | '${result.stdout}\n' 34 | 'Process stderr:\n' 35 | '---------------\n' 36 | '${result.stderr}\n'); 37 | } 38 | expect( 39 | globalPackageIsActiveAndCompatible( 40 | packageName, VersionConstraint.parse(constraint), 41 | environment: environment), 42 | isTrue, 43 | reason: "$packageName should be globally activated, but isn't."); 44 | } 45 | -------------------------------------------------------------------------------- /test/utils/arg_results_utils_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'package:args/args.dart'; 3 | import 'package:dart_dev/src/utils/arg_results_utils.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('flagValue', () { 8 | final flag = 'flag'; 9 | final argParser = ArgParser()..addFlag(flag, defaultsTo: null); 10 | 11 | test('null argResults', () { 12 | expect(flagValue(null, flag), isNull); 13 | }); 14 | 15 | test('null value', () { 16 | final argResults = argParser.parse([]); 17 | expect(flagValue(argResults, flag), isNull); 18 | }); 19 | 20 | test('throws ArgumentError on non-flag value', () { 21 | final argResults = (ArgParser()..addOption(flag)).parse(['--$flag=foo']); 22 | expect(() => flagValue(argResults, flag), throwsArgumentError); 23 | }); 24 | 25 | test('returns value as bool', () { 26 | final argResults = argParser.parse(['--$flag']); 27 | expect(flagValue(argResults, flag), isTrue); 28 | }); 29 | }); 30 | 31 | group('multiOptionValue', () { 32 | final opt = 'opt'; 33 | final argParser = ArgParser()..addMultiOption(opt); 34 | 35 | test('null argResults', () { 36 | expect(multiOptionValue(null, opt), isNull); 37 | }); 38 | 39 | test('throws ArgumentError on non-multi-option value', () { 40 | final argResults = (ArgParser()..addFlag(opt)).parse(['--$opt']); 41 | expect(() => multiOptionValue(argResults, opt), throwsArgumentError); 42 | }); 43 | 44 | test('returns value as Iterable', () { 45 | final argResults = argParser.parse(['--$opt=foo', '--$opt=bar']); 46 | expect(multiOptionValue(argResults, opt), ['foo', 'bar']); 47 | }); 48 | }); 49 | 50 | group('singleOptionValue', () { 51 | final opt = 'opt'; 52 | final argParser = ArgParser()..addOption(opt); 53 | 54 | test('null argResults', () { 55 | expect(singleOptionValue(null, opt), isNull); 56 | }); 57 | 58 | test('null value', () { 59 | final argResults = argParser.parse([]); 60 | expect(singleOptionValue(argResults, opt), isNull); 61 | }); 62 | 63 | test('throws ArgumentError on non-single-option value', () { 64 | final argResults = (ArgParser()..addFlag(opt)).parse(['--$opt']); 65 | expect(() => singleOptionValue(argResults, opt), throwsArgumentError); 66 | }); 67 | 68 | test('returns value as String', () { 69 | final argResults = argParser.parse(['--$opt=foo']); 70 | expect(singleOptionValue(argResults, opt), 'foo'); 71 | }); 72 | }); 73 | 74 | group('splitSingleOptionValue', () { 75 | final opt = 'opt'; 76 | final argParser = ArgParser()..addOption(opt); 77 | 78 | test('null argResults', () { 79 | expect(splitSingleOptionValue(null, opt), isNull); 80 | }); 81 | 82 | test('null value', () { 83 | final argResults = argParser.parse([]); 84 | expect(splitSingleOptionValue(argResults, opt), isNull); 85 | }); 86 | 87 | test('throws ArgumentError on non-single-option value', () { 88 | final argResults = (ArgParser()..addFlag(opt)).parse(['--$opt']); 89 | expect( 90 | () => splitSingleOptionValue(argResults, opt), throwsArgumentError); 91 | }); 92 | 93 | test('returns value as Iterable', () { 94 | final argResults = argParser.parse(['--$opt=foo bar']); 95 | expect(splitSingleOptionValue(argResults, opt), ['foo', 'bar']); 96 | }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /test/utils/assert_no_positional_args_before_separator_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | import 'package:args/args.dart'; 3 | import 'package:dart_dev/src/utils/assert_no_positional_args_before_separator.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | final commandName = 'test'; 8 | bool beforeSeparator = false; 9 | late bool usageExceptionCalled; 10 | void usageException(String msg) { 11 | usageExceptionCalled = true; 12 | expect(msg, contains('The "$commandName" command')); 13 | if (beforeSeparator) { 14 | expect(msg, contains('before the `--` separator')); 15 | } else { 16 | expect(msg, isNot(contains('before the `--` separator'))); 17 | } 18 | } 19 | 20 | setUp(() { 21 | beforeSeparator = false; 22 | usageExceptionCalled = false; 23 | }); 24 | 25 | test('calls usageException callback if assertion fails', () { 26 | final argResults = ArgParser().parse(['positional', 'args']); 27 | assertNoPositionalArgs(commandName, argResults, usageException); 28 | expect(usageExceptionCalled, isTrue); 29 | }); 30 | 31 | test( 32 | 'calls usageException callback if assertion fails (beforeSeparator=true)', 33 | () { 34 | final argResults = ArgParser().parse(['positional', 'args']); 35 | beforeSeparator = true; 36 | assertNoPositionalArgs(commandName, argResults, usageException, 37 | beforeSeparator: true); 38 | expect(usageExceptionCalled, isTrue); 39 | }); 40 | 41 | test('does not call usageException callback if assertion passes', () { 42 | final argResults = ArgParser().parse([]); 43 | assertNoPositionalArgs(commandName, argResults, usageException); 44 | expect(usageExceptionCalled, isFalse); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/utils/dart_dev_paths_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/src/utils/dart_dev_paths.dart'; 2 | import 'package:path/path.dart' as p; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('DartDevPaths', () { 7 | group('posix', () { 8 | late DartDevPaths paths; 9 | 10 | setUp(() { 11 | paths = DartDevPaths(context: p.posix); 12 | }); 13 | 14 | test('cache', () { 15 | expect(paths.cache(), '.dart_tool/dart_dev'); 16 | }); 17 | 18 | test('cache with subpath', () { 19 | expect(paths.cache('sub/path'), '.dart_tool/dart_dev/sub/path'); 20 | }); 21 | 22 | test('config', () { 23 | expect(paths.config, 'tool/dart_dev/config.dart'); 24 | }); 25 | 26 | test('configFromRunScriptForDart', () { 27 | expect(paths.configFromRunScriptForDart, 28 | '../../tool/dart_dev/config.dart'); 29 | }); 30 | 31 | test('legacyConfig', () { 32 | expect(paths.legacyConfig, 'tool/dev.dart'); 33 | }); 34 | 35 | test('runScript', () { 36 | expect(paths.runScript, '.dart_tool/dart_dev/run.dart'); 37 | }); 38 | }); 39 | 40 | group('windows', () { 41 | late DartDevPaths paths; 42 | 43 | setUp(() { 44 | paths = DartDevPaths(context: p.windows); 45 | }); 46 | 47 | test('cache', () { 48 | expect(paths.cache(), r'.dart_tool\dart_dev'); 49 | }); 50 | 51 | test('cache with subpath', () { 52 | expect(paths.cache('sub/path'), r'.dart_tool\dart_dev\sub\path'); 53 | }); 54 | 55 | test('config', () { 56 | expect(paths.config, r'tool\dart_dev\config.dart'); 57 | }); 58 | 59 | test('configFromRunScriptForDart', () { 60 | expect(paths.configFromRunScriptForDart, 61 | r'../../tool/dart_dev/config.dart'); 62 | }); 63 | 64 | test('legacyConfig', () { 65 | expect(paths.legacyConfig, r'tool\dev.dart'); 66 | }); 67 | 68 | test('runScript', () { 69 | expect(paths.runScript, r'.dart_tool\dart_dev\run.dart'); 70 | }); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/utils/format_tool_builder_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | 3 | import 'package:analyzer/dart/analysis/utilities.dart'; 4 | import 'package:dart_dev/dart_dev.dart'; 5 | import 'package:dart_dev/src/tools/over_react_format_tool.dart'; 6 | import 'package:dart_dev/src/utils/format_tool_builder.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | group('FormatToolBuilder', () { 11 | group('detects an OverReactFormatTool correctly', () { 12 | test('when the tool is a MethodInvocation', () { 13 | final visitor = FormatToolBuilder(); 14 | 15 | parseString(content: orfNoCascadeSrc).unit.accept(visitor); 16 | 17 | expect(visitor.formatDevTool, isNotNull); 18 | expect(visitor.formatDevTool, isA()); 19 | }); 20 | 21 | group('when the tool is a CascadeExpression', () { 22 | test('detects line-length', () { 23 | final visitor = FormatToolBuilder(); 24 | 25 | parseString(content: orfCascadeSrc).unit.accept(visitor); 26 | 27 | expect(visitor.formatDevTool, isNotNull); 28 | expect(visitor.formatDevTool, isA()); 29 | expect( 30 | (visitor.formatDevTool as OverReactFormatTool).lineLength, 120); 31 | }); 32 | }); 33 | }); 34 | 35 | group('detects a FormatTool correctly', () { 36 | test('when the tool is a MethodInvocation', () { 37 | final visitor = FormatToolBuilder(); 38 | 39 | parseString(content: formatToolNoCascadeSrc).unit.accept(visitor); 40 | 41 | expect(visitor.formatDevTool, isNotNull); 42 | expect(visitor.formatDevTool, isA()); 43 | }); 44 | 45 | group('when the tool is a CascadeExpression', () { 46 | group('detects formatter correctly for:', () { 47 | test('darfmt', () { 48 | final visitor = FormatToolBuilder(); 49 | 50 | parseString(content: formatToolCascadeSrc()).unit.accept(visitor); 51 | 52 | expect(visitor.formatDevTool, isNotNull); 53 | expect(visitor.formatDevTool, isA()); 54 | expect((visitor.formatDevTool as FormatTool).formatter, 55 | Formatter.dartfmt); 56 | }); 57 | 58 | test('dartFormat', () { 59 | final visitor = FormatToolBuilder(); 60 | 61 | parseString(content: formatToolCascadeSrc(formatter: 'dartFormat')) 62 | .unit 63 | .accept(visitor); 64 | 65 | expect(visitor.formatDevTool, isNotNull); 66 | expect(visitor.formatDevTool, isA()); 67 | expect((visitor.formatDevTool as FormatTool).formatter, 68 | Formatter.dartFormat); 69 | }); 70 | 71 | test('dartStyle', () { 72 | final visitor = FormatToolBuilder(); 73 | 74 | parseString(content: formatToolCascadeSrc(formatter: 'dartStyle')) 75 | .unit 76 | .accept(visitor); 77 | 78 | expect(visitor.formatDevTool, isNotNull); 79 | expect(visitor.formatDevTool, isA()); 80 | expect((visitor.formatDevTool as FormatTool).formatter, 81 | Formatter.dartStyle); 82 | }); 83 | }); 84 | 85 | test('detects formatterArgs', () { 86 | final visitor = FormatToolBuilder(); 87 | 88 | parseString(content: formatToolCascadeSrc()).unit.accept(visitor); 89 | 90 | expect(visitor.formatDevTool, isNotNull); 91 | expect(visitor.formatDevTool, isA()); 92 | expect((visitor.formatDevTool as FormatTool).formatterArgs, 93 | orderedEquals(['-l', '120'])); 94 | }); 95 | }); 96 | }); 97 | 98 | test( 99 | 'sets the failedToDetectAKnownFormatter flag when an unknown FormatTool is being used', 100 | () { 101 | final visitor = FormatToolBuilder(); 102 | 103 | parseString(content: unknownFormatterTool).unit.accept(visitor); 104 | 105 | expect(visitor.formatDevTool, isNull); 106 | expect(visitor.failedToDetectAKnownFormatter, isTrue); 107 | }); 108 | }); 109 | } 110 | 111 | const orfNoCascadeSrc = '''import 'package:dart_dev/dart_dev.dart'; 112 | import 'package:glob/glob.dart'; 113 | 114 | final config = { 115 | ...coreConfig, 116 | 'format': OverReactFormatTool(), 117 | }; 118 | '''; 119 | 120 | const orfCascadeSrc = '''import 'package:dart_dev/dart_dev.dart'; 121 | import 'package:glob/glob.dart'; 122 | 123 | final config = { 124 | ...coreConfig, 125 | 'format': OverReactFormatTool() 126 | ..lineLength = 120, 127 | }; 128 | '''; 129 | 130 | const formatToolNoCascadeSrc = '''import 'package:dart_dev/dart_dev.dart'; 131 | import 'package:glob/glob.dart'; 132 | 133 | final config = { 134 | ...coreConfig, 135 | 'format': FormatTool(), 136 | }; 137 | '''; 138 | 139 | String formatToolCascadeSrc({String formatter = 'dartfmt'}) => 140 | '''import 'package:dart_dev/dart_dev.dart'; 141 | import 'package:glob/glob.dart'; 142 | 143 | final config = { 144 | ...coreConfig, 145 | 'format': FormatTool() 146 | ..formatter = Formatter.$formatter 147 | ..formatterArgs = ['-l', '120'], 148 | }; 149 | '''; 150 | 151 | const unknownFormatterTool = '''import 'package:dart_dev/dart_dev.dart'; 152 | import 'package:glob/glob.dart'; 153 | 154 | final config = { 155 | ...coreConfig, 156 | 'format': UnknownTool() 157 | ..formatter = Formatter.dartfmt 158 | ..formatterArgs = ['-l', '120'], 159 | }; 160 | '''; 161 | -------------------------------------------------------------------------------- /test/utils/get_dart_version_comment_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | @Tags(['dart2']) 3 | import 'package:dart_dev/src/utils/get_dart_version_comment.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('getDartVersionComment returns the version comment in a Dart file', () { 8 | test('', () { 9 | expect( 10 | getDartVersionComment([ 11 | '//@dart=2.9', 12 | '', 13 | 'main() {}', 14 | ].join('\n')), 15 | '//@dart=2.9'); 16 | }); 17 | 18 | test('allowing for whitespace', () { 19 | expect(getDartVersionComment('//@dart=2.9'), '//@dart=2.9'); 20 | expect( 21 | getDartVersionComment('// @dart = 2.9 '), '// @dart = 2.9 '); 22 | }); 23 | 24 | test('regardless of which line it appears on', () { 25 | expect(getDartVersionComment('\n\n//@dart=2.9\n\n'), '//@dart=2.9'); 26 | }); 27 | 28 | test( 29 | 'ignores version comments that don\'t start at the beginning of the line', 30 | () { 31 | const wellFormedVersionComment = '//@dart=2.9'; 32 | expect(getDartVersionComment(wellFormedVersionComment), isNotNull, 33 | reason: 'test setup check'); 34 | 35 | expect(getDartVersionComment(' $wellFormedVersionComment'), isNull); 36 | expect(getDartVersionComment('"$wellFormedVersionComment"'), isNull); 37 | }); 38 | 39 | test('ignores incomplete version comments', () { 40 | expect(getDartVersionComment('//@dart='), isNull); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/utils/organize_imports/organize_directives_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/src/utils/organize_directives/organize_directives.dart'; 2 | import 'package:test/test.dart' show group, test, expect, equals; 3 | 4 | import '../../tools/fixtures/organize_directives/directives.dart'; 5 | 6 | void main() { 7 | const testCases = [ 8 | _TestCase( 9 | '1. sorts dart imports alphabetically', 10 | unorganized1, 11 | organized1, 12 | ), 13 | _TestCase( 14 | '2. sorts pkg imports alphabetically', 15 | unorganized2, 16 | organized2, 17 | ), 18 | _TestCase( 19 | '3. sorts relative imports alphabetically', 20 | unorganized3, 21 | organized3, 22 | ), 23 | _TestCase( 24 | '4. sorts dart above pkg above relative', 25 | unorganized4, 26 | organized4, 27 | ), 28 | _TestCase('5. cleans up double quotes', unorganized5, organized5), 29 | _TestCase( 30 | '6. ignores imports as variables or strings', 31 | unorganized6, 32 | organized6, 33 | ), 34 | _TestCase( 35 | '7. joins split line imports (mixed with other imports)', 36 | unorganized7, 37 | organized7, 38 | ), 39 | _TestCase( 40 | '8. joins split line imports (only split line imports)', 41 | unorganized8, 42 | organized8, 43 | ), 44 | _TestCase( 45 | '9. does not try to sort library directive or @TestOn', 46 | unorganized9, 47 | organized9, 48 | ), 49 | _TestCase( 50 | '10. joins split line imports using show/hide', 51 | unorganized10, 52 | organized10, 53 | ), 54 | _TestCase( 55 | '11. dart, pkg, and relative imports need sorting', 56 | unorganized11, 57 | organized11, 58 | ), 59 | _TestCase( 60 | '12. relative imports sorted correctly when contains `dart` or `package`', 61 | unorganized12, 62 | organized12, 63 | ), 64 | _TestCase('13. preserves comments', unorganized13, organized13), 65 | _TestCase( 66 | '14. preserves comments trailing semi-colon', 67 | unorganized14, 68 | organized14, 69 | ), 70 | _TestCase( 71 | '15. preserves comments above import', 72 | unorganized15, 73 | organized15, 74 | ), 75 | _TestCase( 76 | '16. multiple blank lines between comments', 77 | unorganized16, 78 | organized16, 79 | ), 80 | _TestCase('17. content contains word `import`', unorganized17, organized17), 81 | _TestCase( 82 | '18. preserves implementation imports comment on single line import', 83 | unorganized18, 84 | organized18, 85 | ), 86 | _TestCase('19. multi-line comments', unorganized19, organized19), 87 | _TestCase('20. multi comments on one line', unorganized20, organized20), 88 | _TestCase('21. multiple imports same line', unorganized21, organized21), 89 | _TestCase('22. empty file', unorganized22, organized22), 90 | _TestCase('23. no imports', unorganized23, organized23), 91 | _TestCase('24. comments between imports', unorganized24, organized24), 92 | _TestCase( 93 | '25. comments contains double quotes', 94 | unorganized25, 95 | organized25, 96 | ), 97 | _TestCase('26. exports', unorganized26, organized26), 98 | _TestCase('27. exports using show/hide', unorganized27, organized27), 99 | _TestCase('28. mixed imports and exports', unorganized28, organized28), 100 | _TestCase( 101 | '29. unnecessary new lines between imports and exports', 102 | unorganized29, 103 | organized29, 104 | ), 105 | _TestCase( 106 | '30. unnecessary new lines between all imports and exports', 107 | unorganized30, 108 | organized30, 109 | ), 110 | _TestCase('31. comments with exports', unorganized31, organized31), 111 | _TestCase('32. with dart version comment', unorganized32, organized32), 112 | ]; 113 | 114 | group('organizeDirectives', () { 115 | for (final testCase in testCases) { 116 | test(testCase.description, () { 117 | expect( 118 | organizeDirectives(testCase.unorganized), 119 | equals(testCase.organized), 120 | ); 121 | }); 122 | } 123 | }); 124 | } 125 | 126 | class _TestCase { 127 | final String description; 128 | final String unorganized; 129 | final String organized; 130 | 131 | const _TestCase(this.description, this.unorganized, this.organized); 132 | } 133 | -------------------------------------------------------------------------------- /test/utils/parse_imports_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/src/utils/parse_imports.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | final expectedImportList = [ 6 | 'dart:async', 7 | 'dart:convert', 8 | 'dart:io', 9 | 'package:analyzer/dart/analysis/utilities.dart', 10 | 'package:dart_dev/dart_dev.dart', 11 | 'package:dart_dev/src/tools/over_react_format_tool.dart', 12 | 'package:dart_dev/src/utils/format_tool_builder.dart', 13 | 'package:test/test.dart', 14 | 'utils/assert_dir_is_dart_package.dart', 15 | 'utils/cached_pubspec.dart', 16 | 'utils/dart_dev_paths.dart', 17 | ]; 18 | group( 19 | 'parseImports', 20 | () => test('correctly returns all imports', 21 | () => expect(parseImports(sampleFile), equals(expectedImportList)))); 22 | 23 | group( 24 | 'computePackageNamesFromImports', 25 | () => test( 26 | 'correctly computes package names from imports', 27 | () => expect(computePackageNamesFromImports(expectedImportList), 28 | equals(['analyzer', 'dart_dev', 'test'])))); 29 | } 30 | 31 | const sampleFile = ''' 32 | @TestOn('vm') 33 | import 'dart:async'; 34 | import 'dart:convert'; 35 | import 'dart:io'; 36 | 37 | import 'package:analyzer/dart/analysis/utilities.dart'; 38 | import 'package:dart_dev/dart_dev.dart'; 39 | import 'package:dart_dev/src/tools/over_react_format_tool.dart'; 40 | import 'package:dart_dev/src/utils/format_tool_builder.dart'; 41 | import 'package:test/test.dart'; 42 | 43 | import 'utils/assert_dir_is_dart_package.dart'; 44 | import 'utils/cached_pubspec.dart'; 45 | import 'utils/dart_dev_paths.dart'; 46 | 47 | void main() { 48 | group('FormatToolBuilder', () { 49 | group('detects an OverReactFormatTool correctly', () { 50 | test('when the tool is a MethodInvocation', () { 51 | final visitor = FormatToolBuilder(); 52 | 53 | parseString(content: orfNoCascadeSrc).unit.accept(visitor); 54 | 55 | expect(visitor.formatDevTool, isNotNull); 56 | expect(visitor.formatDevTool, isA()); 57 | }); 58 | }); 59 | }); 60 | } 61 | '''; 62 | -------------------------------------------------------------------------------- /test/utils/rest_args_with_separator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | import 'package:dart_dev/src/utils/rest_args_with_separator.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('restArgsWithSeparator', () { 7 | late ArgParser parser; 8 | 9 | setUp(() { 10 | parser = ArgParser() 11 | ..addOption('output', abbr: 'o') 12 | ..addFlag('verbose', abbr: 'v'); 13 | }); 14 | 15 | test('with no args', () { 16 | final results = parser.parse([]); 17 | expect(restArgsWithSeparator(results), []); 18 | }); 19 | 20 | test('restores the separator to the correct spot', () { 21 | final results = parser.parse([ 22 | 'a', 23 | '-o', 24 | 'out', 25 | '-v', 26 | 'b', 27 | '--', 28 | 'c', 29 | '-d', 30 | ]); 31 | expect(restArgsWithSeparator(results), [ 32 | 'a', 33 | 'b', 34 | '--', 35 | 'c', 36 | '-d', 37 | ]); 38 | }); 39 | 40 | test('with multiple separators', () { 41 | final results = parser 42 | .parse(['a', '-o', 'out', '-v', 'b', '--', 'c', '-d', '--', 'e']); 43 | expect(restArgsWithSeparator(results), [ 44 | 'a', 45 | 'b', 46 | '--', 47 | 'c', 48 | '-d', 49 | '--', 50 | 'e', 51 | ]); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_dev/dart_dev.dart'; 2 | import 'package:glob/glob.dart'; 3 | 4 | final config = { 5 | ...coreConfig, 6 | 'analyze': AnalyzeTool()..useDartAnalyze = true, 7 | 'format': FormatTool() 8 | ..organizeDirectives = true 9 | ..exclude = [Glob('test/**/fixtures/**.dart')] 10 | ..formatter = Formatter.dartFormat, 11 | }; 12 | -------------------------------------------------------------------------------- /tool/file_watchers/ddev_format_on_save.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 22 | --------------------------------------------------------------------------------