diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..20065d84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: There is a problem in how provider behaves +title: "" +labels: bug, needs triage +assignees: + - rrousselGit +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + + + +**Expected behavior** +A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..e5e64dc7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: I have a problem and I need help + url: https://github.com/rrousselGit/riverpod/discussions + about: Pleast ask and answer questions here diff --git a/.github/ISSUE_TEMPLATE/example_request.md b/.github/ISSUE_TEMPLATE/example_request.md new file mode 100644 index 00000000..332337ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/example_request.md @@ -0,0 +1,20 @@ +--- +name: Documentation improvement request +about: >- + Suggest a new example/documentation or ask for clarification about an + existing one. +title: "" +labels: documentation, needs triage +assignees: + - rrousselGit +--- + +**Describe what scenario you think is uncovered by the existing examples/articles** +A clear and concise description of the problem that you want explained. + +**Describe why existing examples/articles do not cover this case** +Explain which examples/articles you have seen before making this request, and +why they did not help you with your problem. + +**Additional context** +Add any other context or screenshots about the documentation request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..65c5ae35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement, needs triage +assignees: + - rrousselGit +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..39bd9ac1 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..47577106 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build + +on: + pull_request: + paths-ignore: + - "**.md" + push: + branches: + - main + paths-ignore: + - "**.md" + schedule: + # runs the CI everyday at 10AM + - cron: "0 10 * * *" + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + channel: + - master + pub: + - get + - upgrade + + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + channel: ${{ matrix.channel }} + + - name: Add pub cache bin to PATH + run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH + + - name: Add pub cache to PATH + run: echo "PUB_CACHE="$HOME/.pub-cache"" >> $GITHUB_ENV + + - name: Add pubspec_overrides to the analyzer_plugin starter + run: "echo \"dependency_overrides:\n custom_lint:\n path: ${{github.workspace}}/packages/custom_lint\" > packages/custom_lint/tools/analyzer_plugin/pubspec_overrides.yaml" + + - run: dart pub global activate melos + + - name: Install dependencies + run: melos exec -- "dart pub ${{ matrix.pub }}" + + - name: Check format + run: dart format --set-exit-if-changed . + + - name: Analyze + run: dart analyze + + - name: Run tests + run: melos exec --dir-exists=test "dart test" + + # - name: Upload coverage to codecov + # run: curl -s https://codecov.io/bash | bash diff --git a/.gitignore b/.gitignore index 7f1f3e57..7b8e06fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ log.txt pubspec.lock +.packages # Ignoring generated files, as they pollute pull requests and can create merge conflicts *.g.dart *.freezed.dart +# Including generated files from the lib folders, as these should be published on pub !packages/*/lib/**/*.freezed.dart +!packages/*/lib/**/*.g.dart # Ignoring native folders of the example as they can be re-generated easily **/example/android/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 27e57ec8..0c37f541 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,10 +3,10 @@ "tasks": [ { "label": "delete .plugin_manager", - "command": "rm -rf /Users/remirousselet/.dartServer/.plugin_manager/ ; echo ' ' > /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/log.txt ; echo ' ' > /Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/log.txt ; echo ' ' > /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/log.txt", + "command": "rm -rf ~/.dartServer/.plugin_manager/", "type": "shell", "presentation": { - "reveal": "silent", + "reveal": "never", } } ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..be9e1872 --- /dev/null +++ b/README.md @@ -0,0 +1,349 @@ +

+

custom_lint

+ Tools for building custom lint rules. +

+ +

+ License +

+ +--- + + + Chat on Discord + + +--- + +## Index + +- [Index](#index) +- [Tutorial](#tutorial) +- [About](#about) +- [Usage](#usage) + - [Creating a custom lint package](#creating-a-custom-lint-package) + - [Using our custom lint package in an application](#using-our-custom-lint-package-in-an-application) + - [Enabling/disabling and configuring lints](#enablingdisabling-and-configuring-lints) + - [Obtaining the list of lints in the CI](#obtaining-the-list-of-lints-in-the-ci) + - [Using the Dart debugger and enabling hot-reload](#using-the-dart-debugger-and-enabling-hot-reload) + - [Testing your plugins](#testing-your-plugins) + - [Testing lints](#testing-lints) + - [Testing quick fixes and assists](#testing-quick-fixes-and-assists) + +## Tutorial + +You can read the latest [blog post](https://invertase.link/b18R) or watch the [advanced use case with custom_lint video](https://invertase.link/RNoz). + +## About + +Lint rules are a powerful way to improve the maintainability of a project. +The more, the merrier! +But while Dart offers a wide variety of lint rules by default, it cannot +reasonably include every possible lint. For example, Dart does not +include lints related to third-party packages. + +Custom_lint fixes that by allowing package authors to write custom lint rules. + +Custom_lint is similar to [analyzer_plugin], but goes deeper by trying to +provide a better developer experience. + +That includes: + +- A command-line to obtain the list of lints in your CI + without having to write a command line yourself. +- A simplified project setup: + No need to deal with the `analyzer` server or error handling. Custom_lint + takes care of that for you, so that you can focus on writing lints. +- Debugger support. + Inspect your lints using the Dart debugger and place breakpoints. +- Supports hot-reload/hot-restart: + Updating the source code of a linter plugin will dynamically restart it, + without having to restart your IDE/analyzer server. +- Built-in support for `// ignore:` and `// ignore_for_file:`. +- Built-in testing mechanism using `// expect_lint`. See [Testing your plugins](#testing-your-plugins) +- Support for `print(...)` and exceptions: + If your plugin somehow throws or print debug messages, custom_lint + will generate a log file with the messages/errors. + +## Usage + +Using custom_lint is split in two parts: + +- how to define a custom_lint package +- how users can install our package in their application to see our newly defined lints + +### Creating a custom lint package + +To create a custom lint, you will need two things: + +- updating your `pubspec.yaml` to include `custom_lint_builder` as a dependency: + + ```yaml + # pubspec.yaml + name: my_custom_lint_package + environment: + sdk: ">=3.0.0 <4.0.0" + + dependencies: + # we will use analyzer for inspecting Dart files + analyzer: + analyzer_plugin: + # custom_lint_builder will give us tools for writing lints + custom_lint_builder: + ``` + +- create a `lib/.dart` file in your project with the following: + + ```dart + import 'package:analyzer/error/listener.dart'; + import 'package:custom_lint_builder/custom_lint_builder.dart'; + + // This is the entrypoint of our custom linter + PluginBase createPlugin() => _ExampleLinter(); + + /// A plugin class is used to list all the assists/lints defined by a plugin. + class _ExampleLinter extends PluginBase { + /// We list all the custom warnings/infos/errors + @override + List getLintRules(CustomLintConfigs configs) => [ + MyCustomLintCode(), + ]; + } + + class MyCustomLintCode extends DartLintRule { + MyCustomLintCode() : super(code: _code); + + /// Metadata about the warning that will show-up in the IDE. + /// This is used for `// ignore: code` and enabling/disabling the lint + static const _code = LintCode( + name: 'my_custom_lint_code', + problemMessage: 'This is the description of our custom lint', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + // Our lint will highlight all variable declarations with our custom warning. + context.registry.addVariableDeclaration((node) { + // "node" exposes metadata about the variable declaration. We could + // check "node" to show the lint only in some conditions. + + // This line tells custom_lint to render a warning at the location of "node". + // And the warning shown will use our `code` variable defined above as description. + reporter.atNode(node, code); + }); + } + } + ``` + +That's it for defining a custom lint package! + +If you're looking for a more advanced example, see the [example](https://github.com/invertase/dart_custom_lint/tree/main/packages/custom_lint/example). +This example implements: + +- a lint appearing on all variables of a specific type +- a quick fix for that lint +- an "assist" for providing refactoring options. + +Let's now use it in an application. + +### Using our custom lint package in an application + +For users to run custom_lint packages, there are a few steps: + +- The application must contain an `analysis_options.yaml` with the following: + + ```yaml + analyzer: + plugins: + - custom_lint + ``` + +- The application also needs to add `custom_lint` and our package(s) as dev + dependency in their application: + + ```yaml + # The pubspec.yaml of an application using our lints + name: example_app + environment: + sdk: ">=3.0.0 <4.0.0" + + dev_dependencies: + custom_lint: + my_custom_lint_package: + ``` + +That's all! +After running `pub get` (and possibly restarting their IDE), users should now +see our custom lints in their Dart files: + +![screenshot of our custom lints in the IDE](https://raw.githubusercontent.com/invertase/dart_custom_lint/main/resources/lint_showcase.png) + +### Enabling/disabling and configuring lints + +By default, custom_lint enables all installed lints. +But chances are you may want to disable one specific lint, +or alternatively, disable all lints besides a few. + +This configuration is done in your `analysis_options.yaml`, +but in a slightly different manner. + +Configurations are placed within a `custom_lint` object, as +followed: + +```yaml +analyzer: + plugins: + - custom_lint + +custom_lint: + rules: + - my_lint_rule: false # disable this rule +``` + +As mentioned before, all lints are enabled by default. + +```yaml +custom_lint: + # Disable all lints by default + enable_all_lint_rules: false + rules: + - my_lint_rule # only enable my_lint_rule +``` + +If you want to change this, you can optionally disable +all lints by default: + +Last but not least, some lint rules may be configurable. +When a lint is configurable, you can configure it in the same place with: + +```yaml +custom_lint: + rules: + - my_lint_rule: + some_parameter: "some value" +``` + +#### Overriding lint error severities + +You can also override the severity of lint rules in the `analysis_options.yaml` file. +This allows you to change INFO level lints to WARNING or ERROR, or vice versa: + +```yaml +custom_lint: + rules: + - my_lint_rule # enable the rule with default severity + errors: + my_lint_rule: error # Override severity to ERROR + another_rule: warning # Override severity to WARNING + third_rule: info # Override severity to INFO + fourth_rule: none # Suppress the lint entirely +``` + +The available severity levels are: `error`, `warning`, `info`, and `none`. + +### Obtaining the list of lints in the CI + +Unfortunately, running `dart analyze` does not pick up our newly defined lints. +We need a separate command for this. + +To do that, users of our custom lint package can run the following inside their terminal: + +```sh +$ dart run custom_lint + lib/main.dart:0:0 • This is the description of our custom lint • my_custom_lint_code +``` + +If you are working on a Flutter project, run `flutter pub run custom_lint` instead. + +### Using the Dart debugger and enabling hot-reload + +By default, custom_lint does enable hot-reload or give you the necessary +information to start the debugger. This is because most users don't need those, +and only lint authors do. + +If you wish to debug lints, you'll have to update your `analysis_options.yaml` as followed: + +```yaml +analyzer: + plugins: + - custom_lint + +custom_lint: + debug: true + # Optional, will cause custom_lint to log its internal debug information + verbose: true +``` + +Then, to debug plugins in custom_lint, you need to connect to plugins using "attach" +mode in your IDE (`cmd+shift+p` + `Debug: attach to Dart process` in VSCode). + +When using this command, you will need a VM service URI provided by custom_lint. + +There are two possible ways to obtain one: + +- if you started your plugin using `custom_lint --watch`, it should be visible + in the console output. +- if your plugin is started by your IDE, you can open the `custom_lint.log` file + that custom_lint created next to the `pubspec.yaml` of your analyzed projects. + +In both cases, what you're looking for is logs similar to: + +``` +The Dart VM service is listening on http://127.0.0.1:60671/9DS43lRMY90=/ +The Dart DevTools debugger and profiler is available at: http://127.0.0.1:60671/9DS43lRMY90=/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A60671%2F9DS43lRMY90%3D%2Fws +``` + +What you'll want is the first URI. In this example, that is `http://127.0.0.1:60671/9DS43lRMY90=/`. +You can then pass this to your IDE, which should now be able to attach to the +plugin. + +### Testing your plugins + +#### Testing lints + +Custom_lint comes with an official testing mechanism for asserting that your +plugins correctly work. + +Testing lints is straightforward: Simply write a file that should contain +lints from your plugin (such as the example folder). Then, using a syntax +similar to `// ignore`, write a `// expect_lint: code` in the line before +your lint: + +```dart +// expect_lint: riverpod_final_provider +var provider = Provider(...); +``` + +When doing this, there are two possible cases: + +- The line after the `expect_lint` correctly contains the expected lint. + In that case, the lint is ignored (similarly to if we used `// ignore`) +- The next line does **not** contain the lint. + In that case, the `expect_lint` comment will have an error. + +This allows testing your plugins by simply running `custom_lint` on your test/example folder. +Then, if any expected lint is missing, the command will fail. But if your plugin correctly +emits the lint, the command will succeed. + +#### Testing quick fixes and assists + +Testing quick fixes and assists is also possible with regular tests by combining them with +`pkg:analyzer` to manually execute the assists or fixes. An example can be found in the +[Riverpod repository](https://github.com/rrousselGit/riverpod/tree/master/packages/riverpod_lint_flutter_test/test/assists). + +--- + +

+ + + +

+ Built and maintained by Invertase. +

+

+ +[analyzer_plugin]: https://pub.dev/packages/analyzer_plugin diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..fad807af --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,28 @@ +## Add built-in lints for highlighting invalid custom_lint & custom_lint_builder usage + +- no `bin/custom_lint.dart` found +- highlight in the IDE on the pubspec.yaml plugins that failed to start + +## Add custom_lint_test + +For simplifying testing plugins + +## Support disabling lint rules inside the analysis_options.yaml + +Such as: + +```yaml +linter: + rules: + require_trailing_commas: false + +custom_lint: + rules: + riverpod_final_provider: false +``` + +## Add support for refactors and fixes + +Instead of being limited to lints + +Bonus point for a `dart run custom_lint --fix` diff --git a/TASKS.md b/TASKS.md deleted file mode 100644 index d3c1bc22..00000000 --- a/TASKS.md +++ /dev/null @@ -1,49 +0,0 @@ -potential lint errors: - -- [ ] no bin found -- [ ] threw during start -- [ ] failed to connect -- [ ] unknown lint rule -- [ ] lint rule threw -- [ ] CLI executed vs local custom_lint version mismatch - - - - -features to have: - -- [x] IDE integration -- [x] support prints -- [x] built-in error reporting -- [ ] add built-in linter for providing warnings if a custom lint package is incorrectly setup -- [ ] CLI for running lints -- [ ] reactive syntax for source change -- [ ] hot reload or hot-restart -- [ ] handle ignores: - - [ ] `// ignore: lint` - - [ ] `// ignore_for_file: lint` - - [ ] `// ignore_for_file: type=lint` -- [ ] testing framework -- [ ] support analysis_options' configs: - - [ ] `import` - - [ ] `exclude` - - [ ] `include` -- [ ] add optional configuration file for changing the entrypoint location & passing options -- [ ] custom lint packages can specify default configs (rules enabled or disabled by default) -- [ ] unknown configs -- [ ] unknown rules - -How to deal with mono-repos? -Maybe: - -``` -packages/ - foo/ - pubspec.yaml -analysis_options.yaml -pubspec.yaml << depends on custom_lint -``` - -Things to consider: - -- a package may not depend on a specific rule diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml index cdbe84e0..7733b5f1 100644 --- a/all_lint_rules.yaml +++ b/all_lint_rules.yaml @@ -3,10 +3,10 @@ linter: - always_declare_return_types - always_put_control_body_on_new_line - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - always_specify_types - always_use_package_imports - annotate_overrides + - annotate_redeclares - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses @@ -18,6 +18,7 @@ linter: - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes + - avoid_final_parameters - avoid_function_literals_in_foreach_calls - avoid_implementing_value_types - avoid_init_to_null @@ -31,8 +32,6 @@ linter: - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - avoid_returning_null_for_void - avoid_returning_this - avoid_setters_without_getters @@ -53,39 +52,56 @@ linter: - cascade_invocations - cast_nullable_to_non_nullable - close_sinks + - collection_methods_unrelated_type + - combinators_ordering - comment_references + - conditional_uri_does_not_exist - constant_identifier_names - control_flow_in_finally - curly_braces_in_flow_control_structures + - dangling_library_doc_comments - depend_on_referenced_packages - deprecated_consistency + - deprecated_member_use_from_same_package - diagnostic_describe_all_properties - directives_ordering + - discarded_futures - do_not_use_environment + - document_ignores - empty_catches - empty_constructor_bodies - empty_statements + - eol_at_end_of_file - exhaustive_cases - file_names - flutter_style_todos - hash_and_equals - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - invalid_runtime_check_with_js_interop_types - join_return_with_assignment - leading_newlines_in_multiline_strings + - library_annotations - library_names - library_prefixes - library_private_types_in_public_api - lines_longer_than_80_chars - - list_remove_unrelated_type - literal_only_boolean_expressions + - matching_super_parameters + - missing_code_block_language_in_doc_comment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons - no_logic_in_create_state - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter @@ -110,7 +126,6 @@ linter: - prefer_constructors_over_static_methods - prefer_contains - prefer_double_quotes - - prefer_equal_for_default_values - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each @@ -142,7 +157,9 @@ linter: - public_member_api_docs - recursive_getters - require_trailing_commas + - secure_pubspec_urls - sized_box_for_whitespace + - sized_box_shrink_expand - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first @@ -153,15 +170,23 @@ linter: - tighten_type_of_initializing_formals - type_annotate_public_apis - type_init_formals + - type_literal_in_constant_pattern - unawaited_futures + - unintended_html_in_doc_comment - unnecessary_await_in_return - unnecessary_brace_in_string_interps + - unnecessary_breaks - unnecessary_const + - unnecessary_constructor_name - unnecessary_final - unnecessary_getters_setters - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_library_name - unnecessary_new - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations @@ -172,9 +197,14 @@ linter: - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main - unrelated_type_equality_checks - unsafe_html - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums - use_full_hex_values_for_flutter_colors - use_function_type_syntax_for_parameters - use_if_null_to_convert_nulls_to_bools @@ -186,6 +216,8 @@ linter: - use_rethrow_when_possible - use_setters_to_change_properties - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters - use_test_throws_matchers - use_to_and_as_if_applicable - valid_regexps diff --git a/analysis_options.yaml b/analysis_options.yaml index c2c1e38e..4e7216a2 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,11 +1,9 @@ include: all_lint_rules.yaml analyzer: - exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" - strong-mode: - implicit-casts: false - implicit-dynamic: false + language: + strict-casts: true + strict-inference: true + strict-raw-types: true errors: # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. # We explicitly enabled even conflicting rules and are fixing the conflict @@ -16,9 +14,6 @@ analyzer: linter: rules: - # temporarily disabled - require_trailing_commas: false - # false positive one_member_abstracts: false @@ -57,10 +52,6 @@ linter: # and `@required Widget child` last. always_put_required_named_parameters_first: false - # `as` is not that bad (especially with the upcoming non-nullable types). - # Explicit exceptions is better than implicit exceptions. - avoid_as: false - # This project doesn't use Flutter-style todos flutter_style_todos: false diff --git a/docs/assists.md b/docs/assists.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/fixes.md b/docs/fixes.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/lints.md b/docs/lints.md new file mode 100644 index 00000000..e69de29b diff --git a/example_app/.dart_tool/package_config.json b/example_app/.dart_tool/package_config.json deleted file mode 100644 index aba5e5cc..00000000 --- a/example_app/.dart_tool/package_config.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "_fe_analyzer_shared", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "analyzer", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "analyzer_plugin", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "args", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "async", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "build", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/build-2.2.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "charcode", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "checked_yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "collection", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "convert", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "crypto", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint", - "rootUri": "../../packages/custom_lint", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "custom_lint_builder", - "rootUri": "../../packages/custom_lint_builder", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "dart_style", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "file", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "glob", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "json_annotation", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "logging", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "meta", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "package_config", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "path", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pub_semver", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pubspec_parse", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "recase", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "riverpod", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/riverpod-1.0.3", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "riverpod_lint", - "rootUri": "../../packages/riverpod_lint", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "source_gen", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_gen-1.2.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "source_span", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "state_notifier", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/state_notifier-0.7.2+1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "string_scanner", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "target_lint", - "rootUri": "../../packages/target_lint", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "term_glyph", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "typed_data", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "uuid", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "watcher", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "example_app", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "2.16" - } - ], - "generated": "2022-03-16T12:41:01.245555Z", - "generator": "pub", - "generatorVersion": "2.16.1" -} diff --git a/example_app/.packages b/example_app/.packages deleted file mode 100644 index f1cdaaea..00000000 --- a/example_app/.packages +++ /dev/null @@ -1,43 +0,0 @@ -# This file is deprecated. Tools should instead consume -# `.dart_tool/package_config.json`. -# -# For more info see: https://dart.dev/go/dot-packages-deprecation -# -# Generated by pub on 2022-03-16 13:41:01.228084. -_fe_analyzer_shared:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0/lib/ -analyzer:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1/lib/ -analyzer_plugin:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0/lib/ -args:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ -async:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ -build:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/build-2.2.1/lib/ -charcode:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ -checked_yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ -collection:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0/lib/ -convert:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ -crypto:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ -custom_lint:../packages/custom_lint/lib/ -custom_lint_builder:../packages/custom_lint_builder/lib/ -dart_style:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2/lib/ -file:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ -glob:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ -json_annotation:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0/lib/ -logging:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/ -meta:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -package_config:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ -path:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ -pub_semver:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/ -pubspec_parse:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0/lib/ -recase:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0/lib/ -riverpod:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/riverpod-1.0.3/lib/ -riverpod_lint:../packages/riverpod_lint/lib/ -source_gen:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_gen-1.2.1/lib/ -source_span:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ -state_notifier:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/state_notifier-0.7.2+1/lib/ -string_scanner:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ -target_lint:../packages/target_lint/lib/ -term_glyph:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ -typed_data:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ -uuid:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6/lib/ -watcher:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ -yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ -example_app:lib/ diff --git a/example_app/analysis_options.yaml b/example_app/analysis_options.yaml deleted file mode 100644 index 01a3c443..00000000 --- a/example_app/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: ../analysis_options.yaml -analyzer: - plugins: - - custom_lint \ No newline at end of file diff --git a/example_app/lib/main copy.dart b/example_app/lib/main copy.dart deleted file mode 100644 index e3726708..00000000 --- a/example_app/lib/main copy.dart +++ /dev/null @@ -1,3 +0,0 @@ -void main() { - print('hello wolrd'); -} diff --git a/example_app/pubspec.lock b/example_app/pubspec.lock deleted file mode 100644 index 522a3678..00000000 --- a/example_app/pubspec.lock +++ /dev/null @@ -1,257 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "36.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.0" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.1" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - custom_lint: - dependency: "direct dev" - description: - path: "../packages/custom_lint" - relative: true - source: path - version: "0.0.1" - custom_lint_builder: - dependency: transitive - description: - path: "../packages/custom_lint_builder" - relative: true - source: path - version: "0.0.1" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.2" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" - logging: - dependency: transitive - description: - name: logging - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - recase: - dependency: transitive - description: - name: recase - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - riverpod: - dependency: "direct main" - description: - name: riverpod - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - riverpod_lint: - dependency: "direct dev" - description: - path: "../packages/riverpod_lint" - relative: true - source: path - version: "0.0.1" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - state_notifier: - dependency: transitive - description: - name: state_notifier - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.2+1" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - target_lint: - dependency: "direct dev" - description: - path: "../packages/target_lint" - relative: true - source: path - version: "0.0.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - uuid: - dependency: transitive - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" -sdks: - dart: ">=2.16.0 <3.0.0" diff --git a/example_app/pubspec.yaml b/example_app/pubspec.yaml deleted file mode 100644 index 64cae7db..00000000 --- a/example_app/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: example_app -version: 0.0.1 -publish_to: none - -environment: - sdk: ">=2.16.0 <3.0.0" - -dependencies: - riverpod: ^1.0.3 - -dev_dependencies: - custom_lint: - path: ../packages/custom_lint - riverpod_lint: - path: ../packages/riverpod_lint - target_lint: - path: ../packages/target_lint diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 00000000..b087ccfa --- /dev/null +++ b/melos.yaml @@ -0,0 +1,6 @@ +name: custom_lint_workspace + +packages: + - packages/custom_lint* + - packages/custom_lint*/example** + - packages/lint_visitor_generator diff --git a/packages/custom_lint/.dart_tool/package_config.json b/packages/custom_lint/.dart_tool/package_config.json deleted file mode 100644 index 4c9f3308..00000000 --- a/packages/custom_lint/.dart_tool/package_config.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "_fe_analyzer_shared", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "analyzer", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "analyzer_plugin", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "args", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "async", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "charcode", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "checked_yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "collection", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "convert", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "crypto", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "dart_style", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "file", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "glob", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "json_annotation", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "meta", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "package_config", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "path", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pub_semver", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pubspec_parse", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "recase", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "source_span", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "string_scanner", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "term_glyph", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "typed_data", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "uuid", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "watcher", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "2.16" - } - ], - "generated": "2022-03-15T15:30:49.329921Z", - "generator": "pub", - "generatorVersion": "2.16.1" -} diff --git a/packages/custom_lint/.packages b/packages/custom_lint/.packages deleted file mode 100644 index d04cb270..00000000 --- a/packages/custom_lint/.packages +++ /dev/null @@ -1,34 +0,0 @@ -# This file is deprecated. Tools should instead consume -# `.dart_tool/package_config.json`. -# -# For more info see: https://dart.dev/go/dot-packages-deprecation -# -# Generated by pub on 2022-03-15 16:30:49.314008. -_fe_analyzer_shared:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0/lib/ -analyzer:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1/lib/ -analyzer_plugin:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0/lib/ -args:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ -async:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ -charcode:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ -checked_yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ -collection:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0/lib/ -convert:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ -crypto:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ -dart_style:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2/lib/ -file:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ -glob:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ -json_annotation:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0/lib/ -meta:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -package_config:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ -path:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ -pub_semver:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/ -pubspec_parse:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0/lib/ -recase:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0/lib/ -source_span:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ -string_scanner:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ -term_glyph:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ -typed_data:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ -uuid:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6/lib/ -watcher:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ -yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ -custom_lint:lib/ diff --git a/packages/custom_lint/.pubignore b/packages/custom_lint/.pubignore new file mode 100644 index 00000000..7e9d7044 --- /dev/null +++ b/packages/custom_lint/.pubignore @@ -0,0 +1,3 @@ +# Ignoring the override file as it is useful for development only +# and require an absolute file path – which is unique to the developer. +tools/analyzer_plugin/pubspec_overrides.yaml diff --git a/packages/custom_lint/CHANGELOG.md b/packages/custom_lint/CHANGELOG.md new file mode 100644 index 00000000..60e613e2 --- /dev/null +++ b/packages/custom_lint/CHANGELOG.md @@ -0,0 +1,393 @@ +## 0.7.5 - 2025-02-27 + +Fix inconsistent version + +## 0.7.4 - 2025-02-27 + +- Upgrade Freezed to 3.0 +- Support Dart workspaces (thanks to @Rexios80) +- Suppport analyzer_plugin 0.13.0 (thanks to @Rexios80) +- Support custom hosted dependencies (thansk to @MobiliteDev) + +## 0.7.3 - 2025-02-08 + +- Bump analyzer_plugin + +## 0.7.2 - 2025-01-29 + +- Fix Android Studio/InteliJ (thanks to @EricSchlichting) + +## 0.7.1 - 2025-01-08 + +- Support analyzer 7.0.0 + +## 0.7.0 - 2024-10-27 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + +## 0.6.10 - 2024-10-10 + +- Support installing custom_lint plugins in `dependencies:` instead of `dev_dependencies` (thanks to @dickermoshe). + +## 0.6.9 - 2024-10-09 + +- `custom_lint_core` upgraded to `0.6.9` + +## 0.6.8 - 2024-10-08 + +- Fix CI +- Fix custom_lint not warning non-Dart files when necessary. +- Custom_lint no-longer tries to analyze projects that lack a `.dart_tool/package_config.json` + +## 0.6.7 - 2024-09-08 + +- Removed offline package resolution for the analyzer plugin. + The logic seemed broken at times, so removing it should make custom_lint more stable. + +## 0.6.6 - 2024-09-08 + +- Fixed an error in the CLI when Flutter generates code under `.dart_tool/` or has dependencies on iOS libraries (thanks to @Kurogoma4D) + +## 0.6.5 - 2024-08-15 + +- Upgraded to analyzer ^6.6.0. + This is a quick fix to unblock the stable Flutter channel. + A more robust fix will come later. +- Fixed a bug where isSuperTypeOf throws if the element is null (thanks to @charlescyt) + +## 0.6.4 - 2024-03-16 + +- Improve error message to attempt debugging a certain bug + +## 0.6.3 - 2024-03-16 + +- Fixed Unimplemented error when running `pub get`. +- Hot-reload and debug mode is now disabled by default. + +## 0.6.2 - 2024-02-19 + +- `custom_lint --format json` no-longer outputs non-JSON logs (thanks to @kzrnm) +- Upgrade analyzer to support 6.4.0 +- Fix null exception when using `TypeChecker.isSuperTypeOf` (thanks to @charlescyt) + +## 0.6.0 - 2024-02-04 + +- Added support for `--fix` + +## 0.5.11 - 2024-01-27 + +- Added support for `analysis_options.yaml` that are nt at the root of the project (thanks to @mrgnhnt96) + +## 0.5.8 - 2024-01-09 + +- `// ignore` comments now correctly respect indentation when they are inserted (thanks to @PiotrRogulski) + +## 0.5.7 - 2023-11-20 + +- Support JSON output format via CLI parameter `--format json|default` (thanks to @kuhnroyal) + +## 0.5.6 - 2023-10-30 + +Optimized logic for finding an unused VM_service port. + +## 0.5.5 - 2023-10-26 + +- Support `hotreloader` 4.0.0 + +## 0.5.4 - 2023-10-20 + +- Sort lints by severity in the command line (thanks to @kuhnroyal) +- Fix watch mode not quitting with `q` (thanks to @kuhnroyal) +- Improve the command line's output (thanks to @kuhnroyal) +- Update uuid to 4.0.0 +- Fixed a port leak +- Fix connection issues on Docker/windows (thanks to @hamsbrar) + +## 0.5.3 - 2023-08-29 + +- The command line now supports ignoring warnings/infos with `--no-fatal-warnings`/`--no-fatal-infos` (thanks to @yamarkz) + +## 0.5.2 - 2023-08-16 + +- Support both analyzer 5.12.0 and 6.0.0 at the same time. +- Attempt at fixing the windows crash + +## 0.5.1 - 2023-08-03 + +Support analyzer v6 + +## 0.5.0 - 2023-06-21 + +- Now resolves packages using `pub get` if custom_lint failed to resolve packages offline. + This should reduce the likelyness of a version conflict in mono-repositories. + The conflict may still happen if two projects in a mono-repo use incompatible + constraints. Like: + ```yaml + name: foo + dependencies: + package: ^1.0.0 + ``` + ```yaml + name: bar + dependencies: + package: ^2.0.0 + ``` +- The command line now shows the lints' severity (thanks to @praxder) +- Now requires Dart 3.0.0 + +## 0.4.0 - 2023-05-12 + +- Report uncaught exceptions inside `context.addPostRunCallback` +- Added support for analyzer 5.12.0 + +## 0.3.4 - 2023-04-19 + +- custom_lint now automatically generate quick-fixes for "ignore for line/file". +- Update the socket communication logic to avoid possible problem is the message + contains a \n. +- fixes custom_lint on windows + +## 0.3.3 - 2023-04-06 + +- Reduce the likelyness of a dependency version conflict. +- Fix `dart analyze` crashing on large projects in the CI due to custom_lint + incorrectly trying to run plugins in debug mode. +- Fix the `custom_lint` command line never terminating in some cases where plugins + fail to start (thanks to @kuhnroyal). +- Upgraded `analyzer` to `>=5.7.0 <5.11.0` +- `LintRuleNodeRegistry` and other AstVisitor-like now are based off `GeneralizingAstVisitor` instead of `GeneralizingAstVisitor` +- Upgraded `cli_util` to `^0.4.0` +- The command line no-longer throws if ran on an empty project or a project with + no plugins enabled +- Exposes the Pubspec in CustomLintContext + +## 0.3.2 - 2023-03-09 + +- Revert "Fixed an issue that caused a "Port already in use" error when + trying to start custom_lint". + This had the opposite effect of what's expected. + +## 0.3.0 - 2023-03-09 + +- Update analyzer to >=5.7.0 <5.8.0 +- Fixed an issue that caused a "Port already in use" error when trying to + start custom_lint + +## 0.2.12 + +Move json_serializable to dev dependencies + +## 0.2.11 + +- Improved the error message when there is a version conflict in mono-repos (thanks to @@adsonpleal) +- Bump minimum Dart SDK to `sdk: ">=2.19.0 <3.0.0"` + +## 0.2.5 + +Fix custom_lint not correctly killing sub-processes when the IDE stops custom_lint. + +## 0.2.2 + +Fixes an exception thrown when a project contains images. + +## 0.2.0 + +**Large Breaking change** +This new version introduces large changes to how lints/fixes/assists are defined. +Long story short, besides the `createPlugin` method, the entire syntax changed. + +See the readme, examples, and docs around how to use the new syntax. + +The new syntax has multiple benefits: + +- It is now possible to enable/disable lints inside the `analysis_options.yaml` + as followed: + + ```yaml + # optional + include: path/to/another/analysis_options.yaml + + custom_lint: + rules: + # enable a lint rule + - my_lint_rule + # A lint rule that is explicitly disabled + - another_lint_rule: false + ``` + + Enabling/disabling lints is supported by default with the new syntax. Nothing to do~ + +- Performance improvement when using a large number of lints. + The workload of analyzing files is now shared between lints. + +- The new syntax makes the code simpler to maintain. + Before, the `PluginBase.getLints` rapidly ended-up doing too much. + Now, it is simple to split the implementation in multiple bits + +## 0.1.2-dev + +Do some internal refactoring as an attempt to fix #60 + +## 0.1.1 + +- Fix an issue where plugins were hot-reloaded when the file analyzed changed. +- Optimized analysis such that `PluginBase.getLints()` is theorically not reinvoked + unless the file analyzed changed. + +## 0.1.0 + +- **Breaking**: The plugin entrypoint has moved. + Plugins no-longer should define a `/bin/custom_lint.dart` file. + Instead they should define a `/lib/.dart` + +- **Breaking**: The plugin entrypoint is modified. Plugins no-longer + define a "main", but instead define a `createPlugin` function: + + Before: + + ```dart + // /bin/custom_lint.dart + void main(List args, SendPort sendPort) { + startPlugin(sendPort, MyPlugin()); + } + ``` + + After: + + ```dart + // /lib/ MyPlugin(); + ``` + +- Add assist support. + Inside your plugins, you can now override `handleGetAssists`: + + ```dart + import 'package:analyzer_plugin/protocol/protocol_generated.dart' + as analyzer_plugin; + + class MyPlugin extends PluginBase { + // ... + + Future handleGetAssists( + ResolvedUnitResult resolvedUnitResult, { + required int offset, + required int length, + }) async { + // TODO return some assists for the given offset + } + } + ``` + +## 0.0.16 + +Fix `expect_lint` not working if the file doesn't contain any lint. + +## 0.0.15 + +- Custom_lint now has a built-in mechanism for testing lints. + Simply write a file that should contain lints for your plugin. + Then, using a syntax similar to `// ignore`, write a `// expect_lint: code` + in the line before your lint: + + ```dart + // expect_lint: riverpod_final_provider + var provider = Provider(...); + ``` + + When doing this, there are two possible cases: + + - The line after the `expect_lint` correctly contains the expected lint. + In that case, the lint is ignored (similarly to if we used `// ignore`) + - The next line does **not** contain the lint. + In that case, the `expect_lint` comment will have an error. + + This allows testing your plugins by simply running `custom_lint` on your test/example folder. + Then, if any expected lint is missing, the command will fail. But if your plugin correctly + emits the lint, the command will succeed. + +- Upgrade analyzer/analzer_plugin + +## 0.0.14 + +- Fix custom_lint not working in the IDE + +## 0.0.13 + +- Add debugger and hot-reload support (Thanks to @TimWhiting) +- Correctly respect `exclude` obtains from the analysis_options.yaml +- Fix `dart analyze` incorrectly failing due to showing the "plugin is starting" lint. + +## 0.0.12 + +- Fix custom_lint plugins not working in release mode and when using git dependencies (thanks to @TimWhiting) +- Fix command line exit code not being set properly (thansk to @andrzejchm) + +## 0.0.11 + +Fix custom_lint not showing in the IDE + +## 0.0.10+1 + +Update docs + +## 0.0.10 + +- Upgrade Riverpod to 2.0.0 +- Fix deprecation errors with analyzer + +## 0.0.9+1 + +Update description and readme + +## 0.0.9 + +- Lint fixes can now be used when placing the cursor on the last character of a lint +- improve pub score + +## 0.0.8 + +Allow lints to emit fixes + +## 0.0.7 + +Fix a bug where the custom_lint command line may not list all lints + +## 0.0.6 + +feat!: getLints now is expected to return a `Stream` instead of `Iterable` + +fix: a bug where the lints shown by the IDE could get out of sync with the actual content of the file + +## 0.0.5 + +Fixed error reporting if a custom_lint plugin throws but the exception comes +from a package instead of the plugin itself. + +## 0.0.4 + +- Fixed a bug where the command line could show IDE-only meant for debugging + +## 0.0.3 + +PluginBase.getLints now receive a `ResolvedUnitResult` instead of a `LibraryElement`. + +## 0.0.2 + +- Compilation errors are now visible within the `pubspec.yaml` of applications + that are using the plugin. + +- Plugins that are currently loading are now highlighted inside the `pubspec.yaml` + of applications that are using the plugin. + +- If a plugin throws when trying to analyze a Dart file, the IDE will now + show the exception at the top of the analyzed file. + +- Compilation errors, exceptions and prints are now accessible within + a log file (`custom_lint.log`) inside applications using the plugin. + +## 0.0.1 + +Initial release diff --git a/packages/custom_lint/LICENSE b/packages/custom_lint/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/packages/custom_lint/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/custom_lint/README.md b/packages/custom_lint/README.md new file mode 120000 index 00000000..fe840054 --- /dev/null +++ b/packages/custom_lint/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/custom_lint/analysis_options.yaml b/packages/custom_lint/analysis_options.yaml deleted file mode 100644 index 4ef3092e..00000000 --- a/packages/custom_lint/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: ../../analysis_options.yaml -analyzer: - plugins: - - custom_lint \ No newline at end of file diff --git a/packages/custom_lint/bin/custom_lint.dart b/packages/custom_lint/bin/custom_lint.dart new file mode 100644 index 00000000..87ac97b6 --- /dev/null +++ b/packages/custom_lint/bin/custom_lint.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:custom_lint/custom_lint.dart'; +import 'package:custom_lint/src/output/output_format.dart'; + +Future entrypoint([List args = const []]) async { + final parser = ArgParser() + ..addFlag( + 'fatal-infos', + help: 'Treat info level issues as fatal', + defaultsTo: true, + ) + ..addFlag( + 'fatal-warnings', + help: 'Treat warning level issues as fatal', + defaultsTo: true, + ) + ..addOption( + 'format', + valueHelp: 'value', + help: 'Specifies the format to display lints.', + defaultsTo: 'default', + allowed: [ + OutputFormatEnum.plain.name, + OutputFormatEnum.json.name, + ], + allowedHelp: { + 'default': + 'The default output format. This format is intended to be user ' + 'consumable.\nThe format is not specified and can change ' + 'between releases.', + 'json': 'A machine readable output in a JSON format.', + }, + ) + ..addFlag( + 'watch', + help: "Watches plugins' sources and perform a hot-reload on change", + negatable: false, + ) + ..addFlag( + 'fix', + help: 'Apply all possible fixes to the lint issues found.', + negatable: false, + ) + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Prints command usage', + ); + final result = parser.parse(args); + + final help = result['help'] as bool; + if (help) { + stdout.writeln('Usage: custom_lint [--watch]'); + stdout.writeln(parser.usage); + return; + } + + final watchMode = result['watch'] as bool; + final fix = result['fix'] as bool; + final fatalInfos = result['fatal-infos'] as bool; + final fatalWarnings = result['fatal-warnings'] as bool; + final format = result['format'] as String; + + await customLint( + workingDirectory: Directory.current, + watchMode: watchMode, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + fix: fix, + format: OutputFormatEnum.fromName(format), + ); +} + +void main([List args = const []]) async { + try { + await entrypoint(args); + } finally { + // TODO figure out why this exit is necessary + exit(exitCode); + } +} diff --git a/packages/custom_lint/build.yaml b/packages/custom_lint/build.yaml new file mode 100644 index 00000000..eeed2647 --- /dev/null +++ b/packages/custom_lint/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + source_gen|combining_builder: + options: + ignore_for_file: + - "type=lint" diff --git a/packages/custom_lint/example/README.md b/packages/custom_lint/example/README.md new file mode 100644 index 00000000..075a67c5 --- /dev/null +++ b/packages/custom_lint/example/README.md @@ -0,0 +1,3 @@ +# Custom Lint Example + +A simple example how powerful is custom_lint package. diff --git a/packages/custom_lint/example/analysis_options.yaml b/packages/custom_lint/example/analysis_options.yaml new file mode 100644 index 00000000..afa5cfd1 --- /dev/null +++ b/packages/custom_lint/example/analysis_options.yaml @@ -0,0 +1,11 @@ +include: ../../../analysis_options.yaml + +analyzer: + plugins: + - custom_lint + +linter: + rules: + public_member_api_docs: false + avoid_print: false + unreachable_from_main: false diff --git a/packages/custom_lint/example/example.md b/packages/custom_lint/example/example.md new file mode 100644 index 00000000..075a67c5 --- /dev/null +++ b/packages/custom_lint/example/example.md @@ -0,0 +1,3 @@ +# Custom Lint Example + +A simple example how powerful is custom_lint package. diff --git a/packages/custom_lint/example/example_lint/analysis_options.yaml b/packages/custom_lint/example/example_lint/analysis_options.yaml new file mode 100644 index 00000000..5a2532a3 --- /dev/null +++ b/packages/custom_lint/example/example_lint/analysis_options.yaml @@ -0,0 +1,6 @@ +# include: ../analysis_options.yaml + +# linter: +# rules: +# public_member_api_docs: false +# avoid_print: false diff --git a/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart b/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart new file mode 100644 index 00000000..6647f974 --- /dev/null +++ b/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart @@ -0,0 +1,183 @@ +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, necessary to support lower analyzer versions + LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// This object is a utility for checking whether a Dart variable is assignable +/// to a given class. +/// +/// In this example, the class checked is `ProviderBase` from `package:riverpod`. +const _providerBaseChecker = TypeChecker.fromName( + 'ProviderBase', + packageName: 'riverpod', +); + +/// This is the entrypoint of our plugin. +/// All plugins must specify a `createPlugin` function in their `lib/.dart` file +PluginBase createPlugin() => _RiverpodLint(); + +/// The class listing all the [LintRule]s and [Assist]s defined by our plugin. +class _RiverpodLint extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + PreferFinalProviders(), + ]; + + @override + List getAssists() => [_ConvertToStreamProvider()]; +} + +/// A custom lint rule. +/// In our case, we want a lint rule which analyzes a Dart file. Therefore we +/// subclass [DartLintRule]. +/// +/// For emitting lints on non-Dart files, subclass [LintRule]. +class PreferFinalProviders extends DartLintRule { + PreferFinalProviders() : super(code: _code); + + /// Metadata about the lint define. This is the code which will show-up in the IDE, + /// and its description.. + static const _code = LintCode( + name: 'riverpod_final_provider', + problemMessage: 'Providers should be declared using the `final` keyword.', + ); + + /// The core logic for our custom lint rule. + /// In our case, it will search over all variables defined in a Dart file and + /// search for the ones that implement a specific type (see [__providerBaseChecker]). + @override + void run( + // This object contains metadata about the analyzed file + CustomLintResolver resolver, + // ErrorReporter is for submitting lints. It contains utilities to specify + // where the lint should show-up. + ErrorReporter reporter, + // This contains various utilities, including tools for inspecting the content + // of Dart files in an efficient manner. + CustomLintContext context, + ) { + // Using this function, we search for [VariableDeclaration] reference the + // analyzed Dart file. + context.registry.addVariableDeclaration((node) { + final element = node.declaredElement; + if (element == null || + element.isFinal || + // We check that the variable is a Riverpod provider + !_providerBaseChecker.isAssignableFromType(element.type)) { + return; + } + + // This emits our lint warning at the location of the variable. + reporter.atElement(element, code); + }); + } + + /// [LintRule]s can optionally specify a list of quick-fixes. + /// + /// Fixes will show-up in the IDE when the cursor is above the warning. And it + /// should contain a message explaining how the warning will be fixed. + @override + List getFixes() => [_MakeProviderFinalFix()]; +} + +/// We define a quick fix for an issue. +/// +/// Our quick fix wants to analyze Dart files, so we subclass [DartFix]. +/// Fox quick-fixes on non-Dart files, see [Fix]. +class _MakeProviderFinalFix extends DartFix { + /// Similarly to [LintRule.run], [Fix.run] is the core logic of a fix. + /// It will take care or proposing edits within a file. + @override + void run( + CustomLintResolver resolver, + // Similar to ErrorReporter, ChangeReporter is an object used for submitting + // edits within a Dart file. + ChangeReporter reporter, + CustomLintContext context, + // This is the warning that was emitted by our [LintRule] and which we are + // trying to fix. + AnalysisError analysisError, + // This is the other warnings in the same file defined by our [LintRule]. + // Useful in case we want to offer a "fix all" option. + List others, + ) { + // Using similar logic as in "PreferFinalProviders", we inspect the Dart file + // to search for variable declarations. + context.registry.addVariableDeclarationList((node) { + // We verify that the variable declaration is where our warning is located + if (!analysisError.sourceRange.intersects(node.sourceRange)) return; + + // We define one edit, giving it a message which will show-up in the IDE. + final changeBuilder = reporter.createChangeBuilder( + message: 'Make provider final', + // This represents how high-low should this quick-fix show-up in the list + // of quick-fixes. + priority: 10, + ); + + // Our edit will consist of editing a Dart file, so we invoke "addDartFileEdit". + // The changeBuilder variable also has utilities for other types of files. + changeBuilder.addDartFileEdit((builder) { + final nodeKeyword = node.keyword; + final nodeType = node.type; + if (nodeKeyword != null) { + // Replace "var x = ..." into "final x = ..."" + + // Using "builder", we can emit changes to a file. + // In this case, addSimpleReplacement is used to override a selection + // with a new content. + builder.addSimpleReplacement( + SourceRange(nodeKeyword.offset, nodeKeyword.length), + 'final', + ); + } else if (nodeType != null) { + // Replace "Type x = ..." into "final Type x = ..." + + // Once again we emit an edit to our file. + // But this time, we add new content without replacing existing content. + builder.addSimpleInsertion(nodeType.offset, 'final '); + } + }); + }); + } +} + +/// Using the same principle as we've seen before, we can define an "assist". +/// +/// The main difference between an [Assist] and a [Fix] is that a [Fix] is associated +/// with a problem. While an [Assist] is a change without an associated problem. +/// +/// These are commonly known as "refactoring". +class _ConvertToStreamProvider extends DartAssist { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + context.registry.addVariableDeclaration((node) { + // Check that the visited node is under the cursor + if (!target.intersects(node.sourceRange)) return; + + // verify that the visited node is a provider, to only show the assist on providers + final element = node.declaredElement; + if (element == null || + element.isFinal || + !_providerBaseChecker.isAssignableFromType(element.type)) { + return; + } + + final changeBuilder = reporter.createChangeBuilder( + priority: 1, + message: 'Convert to StreamProvider', + ); + changeBuilder.addDartFileEdit((builder) { + // + }); + }); + } +} diff --git a/packages/custom_lint/example/example_lint/pubspec.yaml b/packages/custom_lint/example/example_lint/pubspec.yaml new file mode 100644 index 00000000..b51c0102 --- /dev/null +++ b/packages/custom_lint/example/example_lint/pubspec.yaml @@ -0,0 +1,14 @@ +name: custom_lint_example_lint +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + custom_lint_builder: + path: ../../../custom_lint_builder + +dev_dependencies: + custom_lint: diff --git a/packages/custom_lint/example/example_lint/pubspec_overrides.yaml b/packages/custom_lint/example/example_lint/pubspec_overrides.yaml new file mode 100644 index 00000000..dc7d23ca --- /dev/null +++ b/packages/custom_lint/example/example_lint/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_visitor +dependency_overrides: + custom_lint: + path: ../.. + custom_lint_builder: + path: ../../../custom_lint_builder + custom_lint_core: + path: ../../../custom_lint_core diff --git a/example_app/lib/main.dart b/packages/custom_lint/example/lib/main.dart similarity index 64% rename from example_app/lib/main.dart rename to packages/custom_lint/example/lib/main.dart index 1bd4ae7b..2f87b2af 100644 --- a/example_app/lib/main.dart +++ b/packages/custom_lint/example/lib/main.dart @@ -1,13 +1,15 @@ import 'package:riverpod/riverpod.dart'; void main() { - print('hello wolrd'); + print('hello world'); } class Main {} +// expect_lint: riverpod_final_provider ProviderBase provider = Provider((ref) => 0); +// expect_lint: riverpod_final_provider Provider provider2 = Provider((ref) => 0); Object? foo = 42; diff --git a/packages/custom_lint/example/lib/test.jpeg b/packages/custom_lint/example/lib/test.jpeg new file mode 100644 index 00000000..e853902d Binary files /dev/null and b/packages/custom_lint/example/lib/test.jpeg differ diff --git a/packages/custom_lint/example/pubspec.yaml b/packages/custom_lint/example/pubspec.yaml new file mode 100644 index 00000000..47fac126 --- /dev/null +++ b/packages/custom_lint/example/pubspec.yaml @@ -0,0 +1,13 @@ +name: custom_lint_example_app +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + riverpod: ^2.0.0 + +dev_dependencies: + custom_lint: + custom_lint_example_lint: + path: ./example_lint diff --git a/packages/custom_lint/example/pubspec_overrides.yaml b/packages/custom_lint/example/pubspec_overrides.yaml new file mode 100644 index 00000000..53aa5353 --- /dev/null +++ b/packages/custom_lint/example/pubspec_overrides.yaml @@ -0,0 +1,10 @@ +# melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_example_lint,custom_lint_visitor +dependency_overrides: + custom_lint: + path: .. + custom_lint_builder: + path: ../../custom_lint_builder + custom_lint_core: + path: ../../custom_lint_core + custom_lint_example_lint: + path: example_lint diff --git a/packages/custom_lint/lib/basic_runner.dart b/packages/custom_lint/lib/basic_runner.dart new file mode 100644 index 00000000..59b16ca7 --- /dev/null +++ b/packages/custom_lint/lib/basic_runner.dart @@ -0,0 +1,4 @@ +@Deprecated('Import `package:custom_lint/custom_lint.dart` instead') +library; + +export 'custom_lint.dart'; diff --git a/packages/custom_lint/lib/custom_lint.dart b/packages/custom_lint/lib/custom_lint.dart new file mode 100644 index 00000000..000efefa --- /dev/null +++ b/packages/custom_lint/lib/custom_lint.dart @@ -0,0 +1,199 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cli_util/cli_logging.dart'; + +import 'src/cli_logger.dart'; +import 'src/output/output_format.dart'; +import 'src/output/render_lints.dart'; +import 'src/plugin_delegate.dart'; +import 'src/runner.dart'; +import 'src/server_isolate_channel.dart'; +import 'src/v2/custom_lint_analyzer_plugin.dart'; +import 'src/workspace.dart'; + +const _help = ''' + +Custom lint runner commands: +r: Force re-lint +q: Quit + +'''; + +/// Runs plugins with custom_lint.dart on the given directory +/// +/// In watch mode: +/// * This will run until the user types q to quit +/// * The plugin will hot-reload when the user changes it's code, and will cause a re-lint +/// * The exit code is the one from the last lint before quitting +/// * The user can force a reload by typing r +/// +/// Otherwise: +/// * There is no hot-reload or watching so linting only happens once +/// * The process exits with the most recent result of the linter +/// +/// Watch mode cannot be enabled if in release mode. +Future customLint({ + bool watchMode = true, + required Directory workingDirectory, + bool fatalInfos = true, + bool fatalWarnings = true, + OutputFormatEnum format = OutputFormatEnum.plain, + bool fix = false, +}) async { + // Reset the code + exitCode = 0; + + final channel = ServerIsolateChannel(); + try { + await _runServer( + channel, + watchMode: watchMode, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + format: format, + fix: fix, + ); + } catch (_) { + exitCode = 1; + } finally { + await channel.close(); + } +} + +Future _runServer( + ServerIsolateChannel channel, { + required bool watchMode, + required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, + required OutputFormatEnum format, + required bool fix, +}) async { + final customLintServer = await CustomLintServer.start( + sendPort: channel.receivePort.sendPort, + watchMode: watchMode, + fix: fix, + workingDirectory: workingDirectory, + // In the CLI, only show user defined lints. Errors & logs will be + // rendered separately + includeBuiltInLints: false, + delegate: CommandCustomLintDelegate(), + ); + + await CustomLintServer.runZoned(() => customLintServer, () async { + CustomLintRunner? runner; + + try { + final workspace = await CustomLintWorkspace.fromPaths( + [workingDirectory.path], + workingDirectory: workingDirectory, + ); + runner = CustomLintRunner(customLintServer, workspace, channel); + + await runner.initialize; + + final log = CliLogger(); + final progress = + format == OutputFormatEnum.json ? null : log.progress('Analyzing'); + + await _runPlugins( + runner, + log: log, + progress: progress, + reload: false, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + format: format, + ); + + if (watchMode) { + await _startWatchMode( + runner, + log: log, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + format: format, + ); + } + } finally { + await runner?.close(); + } + }).whenComplete(() async { + // Closing the server output of "runZoned" to ensure that "runZoned" completes + // before the server is closed. + // Failing to do so could cause exceptions within "runZoned" to be handled + // after the server is closed, preventing the exception from being printed. + await customLintServer.close(); + }); +} + +Future _runPlugins( + CustomLintRunner runner, { + required Logger log, + required bool reload, + required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, + required OutputFormatEnum format, + Progress? progress, +}) async { + final lints = await runner.getLints(reload: reload); + + renderLints( + lints, + log: log, + progress: progress, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + format: format, + ); +} + +Future _startWatchMode( + CustomLintRunner runner, { + required Logger log, + required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, + required OutputFormatEnum format, +}) async { + if (stdin.hasTerminal) { + stdin + // Let's not pollute the output with whatever the user types + ..echoMode = false + // Let's not force user to have to press "enter" to input a command + ..lineMode = false; + } + + log.stdout(_help); + + // Handle user inputs, forcing the command to continue until the user asks to "quit" + await for (final input in stdin.transform(utf8.decoder)) { + switch (input) { + case 'r': + // Rerunning lints + final progress = log.progress('Manual re-lint'); + await _runPlugins( + runner, + log: log, + progress: progress, + reload: true, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + format: format, + ); + case 'q': + // Let's quit the command line + return; + default: + // Unknown command. Nothing to do + } + } +} diff --git a/packages/custom_lint/lib/protocol.dart b/packages/custom_lint/lib/protocol.dart deleted file mode 100644 index 8698faf8..00000000 --- a/packages/custom_lint/lib/protocol.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:analyzer_plugin/protocol/protocol.dart'; - -class PrintParams { - PrintParams(this.message); - - factory PrintParams.fromNotification(Notification notification) { - assert( - notification.event == key, - 'Notification is not a print notification', - ); - - return PrintParams(notification.params!['message']! as String); - } - - static const key = 'custom_lint.print'; - - final String message; - - Notification toNotification() { - return Notification(key, {'message': message}); - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin.dart b/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin.dart deleted file mode 100644 index f417d40f..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin.dart +++ /dev/null @@ -1,514 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:analyzer/file_system/file_system.dart' as analyzer; -import 'package:analyzer_plugin/protocol/protocol.dart' as plugin; -import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; -import 'package:analyzer_plugin/protocol/protocol_common.dart'; -import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; -import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin - show RequestParams; -import 'package:package_config/package_config_types.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:uuid/uuid.dart'; - -import '../log.dart'; -import 'my_server_plugin.dart'; -import 'plugin_link.dart'; - -const _uui = Uuid(); - -class CustomLintPlugin extends MyServerPlugin { - CustomLintPlugin(analyzer.ResourceProvider provider) : super(provider); - - @override - String get contactInfo => 'https://github.com/invertase/custom_lint/issues'; - - @override - List get fileGlobsToAnalyze => const ['*.dart']; - - @override - String get name => 'custom_lint'; - - @override - String get version => '1.0.0-alpha.0'; - - // Storing the future directly to avoid race conditions - final _pluginLinks = {}; - - /// A table mapping the current context roots to the analysis driver created - /// for that root. - final _activeContextRoots = {}; - - late plugin.PluginVersionCheckParams _versionCheckRequest; - plugin.AnalysisSetPriorityFilesParams? _lastSetPriorityFilesRequest; - - @override - Future handleAnalysisSetContextRoots( - plugin.AnalysisSetContextRootsParams parameters, - ) async { - // Context roots have changed, so we spawn/kill plugins associated with - // the new/removed roots. - - final contextRoots = parameters.roots; - final oldRoots = _activeContextRoots.toList(); - - // Let's spawn new plugins for new context roots - final newPluginKeys = contextRoots.expand((contextRoot) { - // contextRoot was already initialized, so we don't re-create it - if (oldRoots.remove(contextRoot)) return const []; - - _activeContextRoots.add(contextRoot); - return _spawnNewPluginsForContext(contextRoot).keys; - }).toList(); - - // Initializing new plugins, calling version check + set context roots + initial priority files - // TODO use Future.wait - // TODO guard errors - for (final linkKey in newPluginKeys) { - // TODO close subscribption - _pluginLinks[linkKey]! - ..messages.listen((event) { - final file = File('${linkKey.toFilePath()}/log.txt'); - file.writeAsStringSync( - event.message + '\n', - mode: FileMode.append, - ); - }) - ..error.listen((event) { - final file = File('${linkKey.toFilePath()}/log.txt'); - file.writeAsStringSync( - '${event.message}\n${event.stackTrace}\n', - mode: FileMode.append, - ); - }) - ..notifications.listen((event) { - _handleNotification(event, linkKey); - }); - - // TODO what if setContextRoot or priotity files changes while these - // requests are pending? - await _requestPlugin(linkKey, _versionCheckRequest); - - // TODO filter events if the previous/new values are the same - // Call setContextRoots on the plugin with only the roots that have - // the plugin enabled - await _requestPlugin( - linkKey, - plugin.AnalysisSetContextRootsParams( - parameters.roots - .where((contextRoot) => - _pluginLinks[linkKey]!.contextRoots.contains(contextRoot)) - .toList(), - ), - ); - - final priorityFilesParam = _priorityFilesForPlugin(linkKey); - if (priorityFilesParam != null) { - await _requestPlugin(linkKey, priorityFilesParam); - } - } - - // TODO handle context root change on already existing plugins - // Killing unused plugins from removed roots - await Future.wait( - oldRoots.map((contextRoot) { - // The context has been removed, so we kill the plugin. - if (_activeContextRoots.remove(contextRoot)) { - return _disposePluginsForContext(contextRoot); - } - return Future.value(); - }), - ); - - return plugin.AnalysisSetContextRootsResult(); - } - - Future _disposePluginsForContext( - plugin.ContextRoot contextRoot, - ) { - // Remove the context root and stop all plugins that are no-longer - // associated with any context root. - - return Future.wait([ - for (final pluginLink in _pluginLinks.values.toList()) - if (pluginLink.contextRoots.remove(contextRoot) && - pluginLink.contextRoots.isEmpty) - _pluginLinks.remove(pluginLink.key)!.close(), - ]); - } - - Map _spawnNewPluginsForContext( - plugin.ContextRoot pluginContextRoot, - ) { - final result = {}; - - try { - for (final plugin in _getPluginsForContext(pluginContextRoot)) { - if (!_pluginLinks.containsKey(plugin.root)) { - runZonedGuarded(() { - result[plugin.root] = - _pluginLinks[plugin.root] = PluginLink.spawn(plugin.root); - }, (err, stack) { - log('Error while spawning isolate $err \n $stack'); - }); - - final pubspecPath = p.join( - plugin.root.toFilePath(), - 'pubspec.yaml', - ); - - log('warning at $pubspecPath'); - - // try { - // void fn() { - // channel.sendNotification( - // plugin.AnalysisErrorsParams( - // pubspecPath, - // [ - // // plugin.AnalysisError( - // // // TODO use plugin.class - // // AnalysisErrorSeverity.WARNING, - // // AnalysisErrorType.LINT, - // // plugin.Location(pubspecPath, 0, 100, 0, 0), - // // 'Invalid pubspec format', - // // 'custom_lint_setup', - // // ), - // ], - // ).toNotification(), - // ); - // } - - // // Timer(Duration(seconds: 10), fn); - // } catch (err, stack) { - // log('failed to do something $err\n$stack'); - // } - } - - result[plugin.root]!.contextRoots.add(pluginContextRoot); - } - - return result; - } catch (err, stack) { - log('Failed to start plugins2:\n$err\n$stack\n\n'); - channel.sendNotification( - plugin.PluginErrorParams( - true, - err.toString(), - stack.toString(), - ).toNotification(), - ); - rethrow; - } - } - - Iterable _getPluginsForContext( - plugin.ContextRoot contextRoot, - ) sync* { - final packagePath = contextRoot.root; - // TODO if it is a plugin definition, assert that it contains the necessary configs - - // TODO is it safe to assume that there will always be a pubspec at the root? - // TODO will there be packages nested in this directory, or will analyzer_plugin spawn a new plugin? - // TODO should we listen to source changes for pubspec change/creation? - final pubspec = _loadPubspecAt(packagePath); - - log('Got package ${pubspec.name}'); - - final packageConfigFile = File( - p.join(packagePath, '.dart_tool', 'package_config.json'), - ); - - if (!packageConfigFile.existsSync()) { - // TODO should we listen to source changes for a late pub get and reload? - throw StateError( - 'No ${packageConfigFile.path} found. Make sure to run `pub get` first.', - ); - } - - final packageConfig = PackageConfig.parseString( - packageConfigFile.readAsStringSync(), - packageConfigFile.uri, - ); - - for (final dependency in { - ...pubspec.dependencies, - ...pubspec.devDependencies, - ...pubspec.dependencyOverrides - }.entries) { - final dependencyMeta = packageConfig.packages.firstWhere( - (package) => package.name == dependency.key, - orElse: () => throw StateError( - 'Failed to find the source for ${dependency.key}. ' - 'Make sure to run `pub get`.', - ), - ); - - final dependencyPubspec = - _loadPubspecAt(dependencyMeta.root.toFilePath()); - -// TODO extract magic value - if (dependencyPubspec.hasDependency('custom_lint_builder')) { - yield dependencyMeta; - log('found plugin for ${dependency.key}: ${dependencyPubspec.name}'); - // TODO assert that they have the necessary configs - - log('spawning plugin: ${dependencyPubspec.name}'); - } - } - } - - void _handleNotification(plugin.Notification notification, Uri pluginKey) { - // TODO try/catch - switch (notification.event) { - case 'analysis.errors': - final link = _pluginLinks[pluginKey]!; - final params = - plugin.AnalysisErrorsParams.fromNotification(notification); - - if (!p.isAbsolute(params.file)) { - throw StateError('${params.file} is not an absolute path'); - } - - // TODO why are all files re-analyzed when a single file changes? - // TODO handle removed files or there is otherwise a memory leak - link.lintsForLibrary[params.file] = params; - - final lintsForFile = _pluginLinks.values - .expand( - (link) => link.lintsForLibrary[params.file]?.errors ?? const [], - ) - .toList(); - - log('got lints for ${params.file}: ${lintsForFile.map((e) => e.code)}'); - - channel.sendNotification( - plugin.AnalysisErrorsParams( - params.file, - lintsForFile, - ).toNotification(), - ); - break; - default: - channel.sendNotification(notification); - break; - } - } - - Future> _requestAllPlugins( - plugin.RequestParams request, - ) { - return Future.wait( - _pluginLinks.keys.map((key) => _requestPlugin(key, request)), - ); - } - - Future _requestPlugin( - Uri pluginKey, - plugin.RequestParams request, - ) async { - assert( - _pluginLinks.containsKey(pluginKey), - 'Bad state, plugin $pluginKey not found', - ); - final link = _pluginLinks[pluginKey]!; - final id = _uui.v4(); - - final response = link.responses.firstWhere((message) => message.id == id); - link.send(request.toRequest(id).toJson()); - return response; - } - - @override - Future handleEditGetFixes( - plugin.EditGetFixesParams parameters, - ) async { - final responses = await _requestAllPlugins(parameters); - - return plugin.EditGetFixesResult( - responses - .map(plugin.EditGetFixesResult.fromResponse) - .expand((e) => e.fixes) - .toList(), - ); - } - - @override - FutureOr handlePluginVersionCheck( - plugin.PluginVersionCheckParams parameters, - ) { - _versionCheckRequest = parameters; - - final versionString = parameters.version; - final serverVersion = Version.parse(versionString); - // TODO does this needs to be deferred to plugins? - return plugin.PluginVersionCheckResult( - isCompatibleWith(serverVersion), - name, - version, - fileGlobsToAnalyze, - contactInfo: contactInfo, - ); - } - - @override - FutureOr - handleAnalysisHandleWatchEvents( - plugin.AnalysisHandleWatchEventsParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.AnalysisHandleWatchEventsResult(); - } - - plugin.AnalysisSetPriorityFilesParams? _priorityFilesForPlugin( - Uri pluginKey, - ) { - final allPriorityFiles = _lastSetPriorityFilesRequest?.files; - if (allPriorityFiles == null) return null; - - final link = _pluginLinks[pluginKey]; - if (link == null) { - throw StateError('Plugin $pluginKey not found'); - } - - final priorityFilesForPlugin = allPriorityFiles.where( - (priorityFile) { - return link.contextRoots.any( - (contextRoot) => p.isWithin(contextRoot.root, priorityFile), - ); - }, - ).toList(); - - return plugin.AnalysisSetPriorityFilesParams(priorityFilesForPlugin); - } - - @override - FutureOr - handleAnalysisSetPriorityFiles( - plugin.AnalysisSetPriorityFilesParams parameters, - ) async { - // TODO verify priority files are part of the context roots associated with the plugin - _lastSetPriorityFilesRequest = parameters; - - await Future.wait( - _pluginLinks.entries.map( - (entry) => - // TODO filter request if previous/new values are the same - _requestPlugin(entry.key, _priorityFilesForPlugin(entry.key)!), - ), - ); - - return plugin.AnalysisSetPriorityFilesResult(); - } - - @override - FutureOr - handleAnalysisSetSubscriptions( - plugin.AnalysisSetSubscriptionsParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.AnalysisSetSubscriptionsResult(); - } - - @override - FutureOr handleAnalysisUpdateContent( - plugin.AnalysisUpdateContentParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.AnalysisUpdateContentResult(); - } - - @override - FutureOr - handleCompletionGetSuggestions( - plugin.CompletionGetSuggestionsParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.CompletionGetSuggestionsResult( - -1, - -1, - const [], - ); - } - - @override - FutureOr handleEditGetAssists( - plugin.EditGetAssistsParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.EditGetAssistsResult( - const [], - ); - } - - @override - FutureOr - handleEditGetAvailableRefactorings( - plugin.EditGetAvailableRefactoringsParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.EditGetAvailableRefactoringsResult( - const [], - ); - } - - @override - FutureOr handleEditGetRefactoring( - plugin.EditGetRefactoringParams parameters, - ) async { - await _requestAllPlugins(parameters); - return null; - } - - @override - Future handleAnalysisGetNavigation( - plugin.AnalysisGetNavigationParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.AnalysisGetNavigationResult( - [], - [], - [], - ); - } - - @override - FutureOr handleKytheGetKytheEntries( - plugin.KytheGetKytheEntriesParams parameters, - ) async { - await _requestAllPlugins(parameters); - return null; - } - - @override - FutureOr handlePluginShutdown( - plugin.PluginShutdownParams parameters, - ) async { - await _requestAllPlugins(parameters); - return plugin.PluginShutdownResult(); - } -} - -Pubspec _loadPubspecAt(String packagePath) { - final pubspecFile = File(p.join(packagePath, 'pubspec.yaml')); - if (!pubspecFile.existsSync()) { - throw StateError('No pubspec.yaml found at $packagePath.'); - } - - return Pubspec.parse( - pubspecFile.readAsStringSync(), - sourceUrl: pubspecFile.uri, - ); -} - -extension on Pubspec { - bool hasDependency(String name) { - return dependencies.containsKey(name) || - devDependencies.containsKey(name) || - dependencyOverrides.containsKey(name); - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin_starter.dart b/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin_starter.dart deleted file mode 100644 index 7252dfc7..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/analyzer_plugin_starter.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; - -import 'package:analyzer/file_system/physical_file_system.dart'; -import 'package:analyzer_plugin/starter.dart'; -import 'package:custom_lint/src/analyzer_plugin/custom_server_plugin_starter.dart'; - -import '../log.dart'; - -import 'analyzer_plugin.dart'; - -void start(Iterable _, SendPort sendPort) { - log('Start custom_plugin'); - - // Server(sendPort, CustomLintPlugin(PhysicalResourceProvider.INSTANCE)).start(); - MyServerPluginStarter(CustomLintPlugin(PhysicalResourceProvider.INSTANCE)) - .start(sendPort); -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/custom_server_plugin_starter.dart b/packages/custom_lint/lib/src/analyzer_plugin/custom_server_plugin_starter.dart deleted file mode 100644 index a8d16f11..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/custom_server_plugin_starter.dart +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:isolate'; - -import 'package:analyzer_plugin/plugin/plugin.dart'; -import 'package:analyzer_plugin/starter.dart'; - -import 'my_isolate_channel.dart'; -import 'my_server_plugin.dart'; - -/// The [Driver] class represents a single running instance of an analysis -/// server plugin. It is responsible for handling the communications with the -/// server and forwarding requests on to the plugin. -class MyServerPluginStarter implements ServerPluginStarter { - /// The plugin that will be started. - final MyServerPlugin plugin; - - /// Initialize a newly created driver that can be used to start the given - /// plugin. - MyServerPluginStarter(this.plugin); - - @override - void start(SendPort sendPort) { - var channel = PluginIsolateChannel(sendPort); - plugin.start(channel); - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/my_isolate_channel.dart b/packages/custom_lint/lib/src/analyzer_plugin/my_isolate_channel.dart deleted file mode 100644 index 3cc053ec..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/my_isolate_channel.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:isolate'; - -import 'package:analyzer_plugin/channel/channel.dart'; -import 'package:analyzer_plugin/protocol/protocol.dart'; - -class PluginIsolateChannel implements PluginCommunicationChannel { - /// Initialize a newly created channel to communicate with the server. - PluginIsolateChannel(this._sendPort) { - _receivePort = ReceivePort(); - _sendPort.send(_receivePort.sendPort); - } - - /// The port used to send notifications and responses to the server. - final SendPort _sendPort; - - /// The port used to receive requests from the server. - late final ReceivePort _receivePort; - - /// The subscription that needs to be cancelled when the channel is closed. - StreamSubscription? _subscription; - - @override - void close() { - _subscription?.cancel(); - _subscription = null; - } - - @override - void listen( - void Function(Request request) onRequest, { - Function? onError, - void Function()? onDone, - }) { - void onData(Object? data) { - final requestMap = data! as Map; - final request = Request.fromJson(requestMap); - onRequest(request); - } - - if (_subscription != null) { - throw StateError('Only one listener is allowed per channel'); - } - _subscription = _receivePort.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: false, - ); - } - - @override - void sendNotification(Notification notification) { - final json = notification.toJson(); - _sendPort.send(json); - } - - @override - void sendResponse(Response response) { - final json = response.toJson(); - _sendPort.send(json); - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/my_server_plugin.dart b/packages/custom_lint/lib/src/analyzer_plugin/my_server_plugin.dart deleted file mode 100644 index c7357891..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/my_server_plugin.dart +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; - -import 'package:analyzer/file_system/file_system.dart'; -import 'package:analyzer/file_system/overlay_file_system.dart'; -import 'package:analyzer/file_system/physical_file_system.dart'; -import 'package:analyzer_plugin/channel/channel.dart'; -import 'package:analyzer_plugin/protocol/protocol.dart'; -import 'package:analyzer_plugin/protocol/protocol_common.dart'; -import 'package:analyzer_plugin/protocol/protocol_constants.dart'; -import 'package:analyzer_plugin/protocol/protocol_generated.dart'; - -// ignore: implementation_imports -import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' - show ResponseResult; -import 'package:pub_semver/pub_semver.dart'; - -import '../log.dart'; - -/// The abstract superclass of any class implementing a plugin for the analysis -/// server. -/// -/// Clients may not implement or mix-in this class, but are expected to extend -/// it. -abstract class MyServerPlugin { - /// Initialize a newly created analysis server plugin. If a resource [provider] - /// is given, then it will be used to access the file system. Otherwise a - /// resource provider that accesses the physical file system will be used. - MyServerPlugin(ResourceProvider? provider) - : resourceProvider = OverlayResourceProvider( - provider ?? PhysicalResourceProvider.INSTANCE, - ); - - /// A megabyte. - static const int M = 1024 * 1024; - - /// The communication channel being used to communicate with the analysis - /// server. - late PluginCommunicationChannel _channel; - - /// The resource provider used to access the file system. - final OverlayResourceProvider resourceProvider; - - /// Return the communication channel being used to communicate with the - /// analysis server, or `null` if the plugin has not been started. - PluginCommunicationChannel get channel => _channel; - - /// Return the user visible information about how to contact the plugin authors - /// with any problems that are found, or `null` if there is no contact info. - String? get contactInfo => null; - - /// Return a list of glob patterns selecting the files that this plugin is - /// interested in analyzing. - List get fileGlobsToAnalyze; - - /// Return the user visible name of this plugin. - String get name; - - /// Return the version number of the plugin spec required by this plugin, - /// encoded as a string. - String get version; - - /// Handle an 'analysis.getNavigation' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisGetNavigation( - AnalysisGetNavigationParams params, - ); - - /// Handle an 'analysis.handleWatchEvents' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisHandleWatchEvents( - AnalysisHandleWatchEventsParams parameters, - ); - - /// Handle an 'analysis.setContextRoots' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisSetContextRoots( - AnalysisSetContextRootsParams parameters, - ); - - /// Handle an 'analysis.setPriorityFiles' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisSetPriorityFiles( - AnalysisSetPriorityFilesParams parameters, - ); - - /// Handle an 'analysis.setSubscriptions' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisSetSubscriptions( - AnalysisSetSubscriptionsParams parameters, - ); - - /// Handle an 'analysis.updateContent' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleAnalysisUpdateContent( - AnalysisUpdateContentParams parameters, - ); - - /// Handle a 'completion.getSuggestions' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleCompletionGetSuggestions( - CompletionGetSuggestionsParams parameters, - ); - - /// Handle an 'edit.getAssists' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleEditGetAssists( - EditGetAssistsParams parameters, - ); - - /// Handle an 'edit.getAvailableRefactorings' request. Subclasses that override - /// this method in order to participate in refactorings must also override the - /// method [handleEditGetRefactoring]. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr - handleEditGetAvailableRefactorings( - EditGetAvailableRefactoringsParams parameters, - ); - - /// Handle an 'edit.getFixes' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleEditGetFixes( - EditGetFixesParams parameters, - ); - - /// Handle an 'edit.getRefactoring' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleEditGetRefactoring( - EditGetRefactoringParams parameters, - ); - - /// Handle a 'kythe.getKytheEntries' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handleKytheGetKytheEntries( - KytheGetKytheEntriesParams parameters, - ); - - /// Handle a 'plugin.shutdown' request. Subclasses can override this method to - /// perform any required clean-up, but cannot prevent the plugin from shutting - /// down. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handlePluginShutdown( - PluginShutdownParams parameters, - ) async { - return PluginShutdownResult(); - } - - /// Handle a 'plugin.versionCheck' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - FutureOr handlePluginVersionCheck( - PluginVersionCheckParams parameters, - ); - - /// Return `true` if this plugin is compatible with an analysis server that is - /// using the given version of the plugin API. - bool isCompatibleWith(Version serverVersion) => - serverVersion <= Version.parse(version); - - /// The method that is called when the analysis server closes the communication - /// channel. This method will not be invoked under normal conditions because - /// the server will send a shutdown request and the plugin will stop listening - /// to the channel before the server closes the channel. - void onDone() {} - - /// The method that is called when an error has occurred in the analysis - /// server. This method will not be invoked under normal conditions. - void onError(Object exception, StackTrace stackTrace) {} - - /// Start this plugin by listening to the given communication [channel]. - void start(PluginCommunicationChannel channel) { - _channel = channel; - _channel.listen(_onRequest, onError: onError, onDone: onDone); - } - - /// Compute the response that should be returned for the given [request], or - /// `null` if the response has already been sent. - Future _getResponse(Request request, int requestTime) async { - ResponseResult? result; - switch (request.method) { - case ANALYSIS_REQUEST_GET_NAVIGATION: - final params = AnalysisGetNavigationParams.fromRequest(request); - result = await handleAnalysisGetNavigation(params); - break; - case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: - final params = AnalysisHandleWatchEventsParams.fromRequest(request); - result = await handleAnalysisHandleWatchEvents(params); - break; - case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: - final params = AnalysisSetContextRootsParams.fromRequest(request); - result = await handleAnalysisSetContextRoots(params); - break; - case ANALYSIS_REQUEST_SET_PRIORITY_FILES: - final params = AnalysisSetPriorityFilesParams.fromRequest(request); - result = await handleAnalysisSetPriorityFiles(params); - break; - case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS: - final params = AnalysisSetSubscriptionsParams.fromRequest(request); - result = await handleAnalysisSetSubscriptions(params); - break; - case ANALYSIS_REQUEST_UPDATE_CONTENT: - final params = AnalysisUpdateContentParams.fromRequest(request); - result = await handleAnalysisUpdateContent(params); - break; - case COMPLETION_REQUEST_GET_SUGGESTIONS: - final params = CompletionGetSuggestionsParams.fromRequest(request); - result = await handleCompletionGetSuggestions(params); - break; - case EDIT_REQUEST_GET_ASSISTS: - final params = EditGetAssistsParams.fromRequest(request); - result = await handleEditGetAssists(params); - break; - case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS: - final params = EditGetAvailableRefactoringsParams.fromRequest(request); - result = await handleEditGetAvailableRefactorings(params); - break; - case EDIT_REQUEST_GET_FIXES: - final params = EditGetFixesParams.fromRequest(request); - result = await handleEditGetFixes(params); - break; - case EDIT_REQUEST_GET_REFACTORING: - final params = EditGetRefactoringParams.fromRequest(request); - result = await handleEditGetRefactoring(params); - break; - case KYTHE_REQUEST_GET_KYTHE_ENTRIES: - final params = KytheGetKytheEntriesParams.fromRequest(request); - result = await handleKytheGetKytheEntries(params); - break; - case PLUGIN_REQUEST_SHUTDOWN: - final params = PluginShutdownParams(); - result = await handlePluginShutdown(params); - _channel.sendResponse(result.toResponse(request.id, requestTime)); - _channel.close(); - return null; - case PLUGIN_REQUEST_VERSION_CHECK: - final params = PluginVersionCheckParams.fromRequest(request); - result = await handlePluginVersionCheck(params); - break; - } - if (result == null) { - return Response( - request.id, - requestTime, - error: RequestErrorFactory.unknownRequest(request.method), - ); - } - return result.toResponse(request.id, requestTime); - } - - /// The method that is called when a [request] is received from the analysis - /// server. - Future _onRequest(Request request) async { - final requestTime = DateTime.now().millisecondsSinceEpoch; - final id = request.id; - Response? response; - try { - response = await _getResponse(request, requestTime); - } on RequestFailure catch (exception) { - response = Response(id, requestTime, error: exception.error); - } catch (exception, stackTrace) { - response = Response( - id, - requestTime, - error: RequestError( - RequestErrorCode.PLUGIN_ERROR, - exception.toString(), - stackTrace: stackTrace.toString(), - ), - ); - } - if (response != null) { - _channel.sendResponse(response); - } - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin/plugin_link.dart b/packages/custom_lint/lib/src/analyzer_plugin/plugin_link.dart deleted file mode 100644 index 57043229..00000000 --- a/packages/custom_lint/lib/src/analyzer_plugin/plugin_link.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; - -import 'package:path/path.dart' as p; -import 'package:analyzer/file_system/file_system.dart' as analyzer; -import 'package:analyzer_plugin/protocol/protocol.dart' as plugin; -import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; -import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; -import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin - show RequestParams; - -import '../../protocol.dart'; -import '../log.dart'; - -class PluginLink { - PluginLink._( - this._isolate, - this._sendPort, - this._responsesController, - this._notificationsController, - this._messagesController, - this._errorController, - this._receivePort, - this.key, - ); - - factory PluginLink.spawn(Uri pluginRootUri) { - // TODO configure that through build.yaml-like file - final mainPath = Uri.file( - p.join(pluginRootUri.toFilePath(), 'lib', 'main.dart'), - ); - - final receivePort = ReceivePort(); - - final isolate = Isolate.spawnUri( - mainPath, - const [], - receivePort.sendPort, - automaticPackageResolution: true, - ); - -// // TODO do we ca re about killing isolates before _listenIsolate completes? - - final errors = StreamController.broadcast(); - final messages = StreamController.broadcast(); - final responses = StreamController.broadcast(); - final notifications = StreamController.broadcast(); - final sendPortCompleter = Completer(); - - // TODO close subscribption - receivePort.listen( - (Object? obj) { - if (obj is SendPort) { - sendPortCompleter.complete(obj); - return; - } - - try { - final json = Map.from(obj! as Map); - - if (json.containsKey(plugin.Notification.EVENT)) { - final notification = plugin.Notification.fromJson(json); - - switch (json[plugin.Notification.EVENT]) { - case PrintParams.key: - final print = PrintParams.fromNotification(notification); - messages.add(print); - break; - case 'plugin.error': - final error = - plugin.PluginErrorParams.fromNotification(notification); - errors.add(error); - break; - default: - notifications.add(notification); - } - } else { - final response = plugin.Response.fromJson(json); - responses.add(response); - } - } catch (err, stack) { - log('failed to decode message $obj with:\n$err\n$stack'); - // TODO handle - } - }, - // TODO handle errors - onDone: () { - errors.close(); - messages.close(); - responses.close(); - notifications.close(); - }, - ); - - return PluginLink._( - isolate, - sendPortCompleter.future, - responses, - notifications, - messages, - errors, - receivePort, - pluginRootUri, - ); - } - - final Uri key; - final Future _isolate; - final Future _sendPort; - final ReceivePort _receivePort; - final contextRoots = {}; - final lintsForLibrary = {}; - - final StreamController _messagesController; - Stream get messages => _messagesController.stream; - - final StreamController _errorController; - Stream get error => _errorController.stream; - - final StreamController _responsesController; - Stream get responses => _responsesController.stream; - - final StreamController _notificationsController; - Stream get notifications => - _notificationsController.stream; - - void send(Map json) { - _sendPort.then((value) => value.send(json)); - } - - Future close() async { - _receivePort.close(); - _messagesController.close(); - _errorController.close(); - _notificationsController.close(); - _responsesController.close(); - return _isolate.then((i) => i.kill()); - } -} diff --git a/packages/custom_lint/lib/src/analyzer_plugin_starter.dart b/packages/custom_lint/lib/src/analyzer_plugin_starter.dart new file mode 100644 index 00000000..ad669e26 --- /dev/null +++ b/packages/custom_lint/lib/src/analyzer_plugin_starter.dart @@ -0,0 +1,27 @@ +import 'dart:io'; +import 'dart:isolate'; + +import 'package:ci/ci.dart' as ci; + +import 'plugin_delegate.dart'; +import 'v2/custom_lint_analyzer_plugin.dart'; + +/// Connects custom_lint to the analyzer server using the analyzer_plugin protocol +Future start(Iterable _, SendPort sendPort) async { + final isInCI = ci.isCI; + + await CustomLintServer.start( + sendPort: sendPort, + includeBuiltInLints: true, + // The IDE client should write to files, as what's visible in the editor + // may not be the same as what's on disk. + fix: false, + // "start" may be run by `dart analyze`, in which case we don't want to + // enable watch mode. There's no way to detect this, but this only matters + // in the CI. So we disable watch mode if we detect that we're in CI. + // TODO enable hot-restart only if running plugin from source (excluding pub cache) + watchMode: isInCI ? false : null, + delegate: AnalyzerPluginCustomLintDelegate(), + workingDirectory: Directory.current, + ); +} diff --git a/packages/custom_lint/lib/src/analyzer_utils/analyzer_utils.dart b/packages/custom_lint/lib/src/analyzer_utils/analyzer_utils.dart new file mode 100644 index 00000000..2bebab27 --- /dev/null +++ b/packages/custom_lint/lib/src/analyzer_utils/analyzer_utils.dart @@ -0,0 +1,33 @@ +import 'package:analyzer/file_system/file_system.dart'; +// ignore: implementation_imports, not exported +import 'package:analyzer/src/dart/analysis/byte_store.dart'; +// ignore: implementation_imports, not exported +import 'package:analyzer/src/dart/analysis/file_byte_store.dart'; + +/// Adds [createByteStore]. +extension CreateByteStore on ResourceProvider { + /// Obtains the location of a [ByteStore]. + String getByteStorePath(String pluginID) { + final stateLocation = getStateLocation(pluginID); + + if (stateLocation == null) { + throw StateError('Failed to obtain the byte store path'); + } + + return stateLocation.path; + } + + /// If the state location can be accessed, return the file byte store, + /// otherwise return the memory byte store. + ByteStore createByteStore(String pluginID) { + const M = 1024 * 1024; + + return MemoryCachingByteStore( + FileByteStore( + getByteStorePath(pluginID), + tempNameSuffix: DateTime.now().millisecondsSinceEpoch.toString(), + ), + 64 * M, + ); + } +} diff --git a/packages/custom_lint/lib/src/async_operation.dart b/packages/custom_lint/lib/src/async_operation.dart new file mode 100644 index 00000000..6eebb40f --- /dev/null +++ b/packages/custom_lint/lib/src/async_operation.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +/// An extension on [Stream] that adds a [safeFirst] method. +extension StreamFirst on Stream { + /// A fork of [first] meant to be used instead of [first], to possibly override + /// it during debugging to provide more information. + Future get safeFirst => first; +} + +/// A class for awaiting multiple async operations at once. +/// +/// See [wait]. +class PendingOperation { + final _pendingOperations = >[]; + + /// Register an async operation to be awaited. + Future run(Future Function() cb) async { + final future = cb(); + + _pendingOperations.add(future); + try { + return await future; + } finally { + _pendingOperations.remove(future); + } + } + + /// Waits for all operations registered in [run]. + /// + /// If during the wait new async operations are registered, they will be + /// awaited too. + Future wait() async { + /// Wait for all pending operations to complete and check that no new + /// operations are queued for a few consecutive frames. + while (_pendingOperations.isNotEmpty) { + await Future.wait(_pendingOperations.toList()) + // Catches errors to make sure that errors inside operations don't + // abort the "wait" early + .then((value) => null, onError: (_) {}); + } + } +} + +/// Workaround to a limitation in [runZonedGuarded] that states the following: +/// +/// > The zone will always be an error-zone ([Zone.errorZone]), so returning a +/// > future created inside the zone, and waiting for it outside of the zone, +/// > will risk the future not being seen to complete. +/// +/// This function solves the issue by creating a [Completer] outside of +/// [runZonedGuarded] and completing it inside the zone. This way, the future +/// is created outside of the zone and can safely be awaited. +Future asyncRunZonedGuarded( + FutureOr Function() body, + void Function(Object error, StackTrace stack) onError, { + Map? zoneValues, + ZoneSpecification? zoneSpecification, +}) async { + final completer = Completer(); + + unawaited( + runZonedGuarded( + () => Future(body).then( + completer.complete, + // ignore: avoid_types_on_closure_parameters, false positive + onError: (Object error, StackTrace stack) { + // Make sure the initial error is also reported. + onError(error, stack); + + completer.completeError(error, stack); + }, + ), + onError, + zoneSpecification: zoneSpecification, + ), + ); + + return completer.future; +} diff --git a/packages/custom_lint/lib/src/channels.dart b/packages/custom_lint/lib/src/channels.dart new file mode 100644 index 00000000..b3b59ce2 --- /dev/null +++ b/packages/custom_lint/lib/src/channels.dart @@ -0,0 +1,156 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +// ignore: implementation_imports, not exported +import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' + show ResponseResult; + +/// An interface for interacting with analyzer_plugin +abstract class AnalyzerPluginClientChannel { + /// The list of messages sent by analyzer_plugin. + Stream get messages; + + /// Sends a JSON object to the analyzer_plugin server + void sendJson(Map json); + + /// Sends a [Response] to the analyzer_plugin server. + void sendResponse({ + ResponseResult? data, + RequestError? error, + required String requestID, + required int requestTime, + }) { + sendJson( + Response( + requestID, + requestTime, + result: data?.toJson(), + error: error, + ).toJson(), + ); + } + + /// Releases the resources + Future close(); +} + +/// The number of bytes used to store the length of a message +const _lengthBytes = 4; + +/// An interface for discussing with analyzer_plugin using a [SendPort] +class JsonSendPortChannel extends AnalyzerPluginClientChannel { + /// An interface for discussing with analyzer_plugin using a [SendPortƒ] + JsonSendPortChannel(this._sendPort) : _receivePort = ReceivePort() { + _sendPort.send(_receivePort.sendPort); + } + + final SendPort _sendPort; + final ReceivePort _receivePort; + + @override + late final Stream messages = _receivePort.asBroadcastStream(); + + @override + void sendJson(Map json) { + _sendPort.send(json); + } + + @override + Future close() async { + _receivePort.close(); + } +} + +/// An interface for discussing with analyzer_plugin using web sockets +class JsonSocketChannel extends AnalyzerPluginClientChannel { + /// An interface for discussing with analyzer_plugin using web sockets + JsonSocketChannel(this._socket) { + // Pipe the socket messages in a broadcast stream + _subscription = Stream.fromFuture(_socket).asyncExpand((e) => e).listen( + _controller.add, + onError: _controller.addError, + onDone: _controller.close, + ); + } + + final Future _socket; + + final _controller = StreamController.broadcast(); + late StreamSubscription _subscription; + + /// Send a message while having the first 4 bytes of the message be the length of the message. + void _sendWithLength(Socket socket, List data) { + final length = data.length; + final buffer = Uint8List(_lengthBytes + length); + final byteData = ByteData.view(buffer.buffer); + + byteData.setUint32(0, length); + buffer.setRange(_lengthBytes, _lengthBytes + length, data); + socket.add(buffer); + } + + /// The [sendJson] method have messages start with the message length, + /// because a chunk of data can contain multiple separate messages. + /// + /// By sending the length with every message, the receiver can know + /// where a message ends and another begins. + Iterable> _receiveWithLength(Uint8List input) sync* { + final chunk = ByteData.view(input.buffer); + + var startOffset = 0; + var bytesCountNeeded = _lengthBytes; + var isReadingMessageLength = true; + + while (startOffset + bytesCountNeeded <= input.length) { + if (isReadingMessageLength) { + // Reading the length of the next message. + bytesCountNeeded = chunk.getUint32(startOffset); + + // We have the message length, now reading the message. + startOffset += _lengthBytes; + isReadingMessageLength = false; + } else { + // We have the message length, now reading the message. + final message = input.sublist( + startOffset, + startOffset + bytesCountNeeded, + ); + yield message; + + // Reset to reading the length of the next message. + startOffset += bytesCountNeeded; + bytesCountNeeded = _lengthBytes; + isReadingMessageLength = true; + } + } + } + + @override + late final Stream messages = _controller.stream + .expand(_receiveWithLength) + .map(utf8.decode) + .map(jsonDecode); + + @override + Future sendJson(Map json) async { + final socket = await _socket; + + _sendWithLength( + socket, + utf8.encode(jsonEncode(json)), + ); + } + + @override + Future close() async { + await Future.wait([ + _subscription.cancel(), + _controller.close(), + ]); + } +} diff --git a/packages/custom_lint/lib/src/cli_logger.dart b/packages/custom_lint/lib/src/cli_logger.dart new file mode 100644 index 00000000..62e315db --- /dev/null +++ b/packages/custom_lint/lib/src/cli_logger.dart @@ -0,0 +1,103 @@ +import 'dart:io' as io; + +import 'package:cli_util/cli_logging.dart'; + +/// Temporary copy of [StandardLogger] from `cli_util` package with a fix +/// for https://github.com/dart-lang/cli_util/pull/87 +/// which replaces print with stdout.writeln. +class CliLogger implements Logger { + /// Creates a cli logger with ANSI support + /// that writes messages and progress [io.stdout]. + CliLogger({Ansi? ansi}) : ansi = ansi ?? Ansi(io.stdout.supportsAnsiEscapes); + + @override + Ansi ansi; + + @override + bool get isVerbose => false; + + Progress? _currentProgress; + + @override + void stderr(String message) { + _cancelProgress(); + + io.stderr.writeln(message); + } + + @override + void stdout(String message) { + _cancelProgress(); + + io.stdout.writeln(message); + } + + @override + void trace(String message) {} + + @override + void write(String message) { + _cancelProgress(); + + io.stdout.write(message); + } + + @override + void writeCharCode(int charCode) { + _cancelProgress(); + + io.stdout.writeCharCode(charCode); + } + + void _cancelProgress() { + final progress = _currentProgress; + if (progress != null) { + _currentProgress = null; + progress.cancel(); + } + } + + @override + Progress progress(String message) { + _cancelProgress(); + + final progress = _LineOnFinishProgress( + ansi.useAnsi + ? AnsiProgress(ansi, message) + : SimpleProgress(this, message), + log: this, + ); + _currentProgress = progress; + return progress; + } + + @override + @Deprecated('This method will be removed in the future') + void flush() {} +} + +class _LineOnFinishProgress implements Progress { + const _LineOnFinishProgress(this.impl, {required this.log}); + + final CliLogger log; + final Progress impl; + + @override + Duration get elapsed => impl.elapsed; + + @override + String get message => impl.message; + + @override + void cancel() { + impl.cancel(); + } + + @override + void finish({String? message, bool showTiming = false}) { + impl.finish(message: message, showTiming: showTiming); + + // Separate progress from results + log.stdout(''); + } +} diff --git a/packages/custom_lint_builder/lib/src/analyzer_plugin/isolate_channel.dart b/packages/custom_lint/lib/src/client_isolate_channel.dart similarity index 79% rename from packages/custom_lint_builder/lib/src/analyzer_plugin/isolate_channel.dart rename to packages/custom_lint/lib/src/client_isolate_channel.dart index 3cc053ec..31289ce2 100644 --- a/packages/custom_lint_builder/lib/src/analyzer_plugin/isolate_channel.dart +++ b/packages/custom_lint/lib/src/client_isolate_channel.dart @@ -1,16 +1,16 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - import 'dart:async'; import 'dart:isolate'; import 'package:analyzer_plugin/channel/channel.dart'; import 'package:analyzer_plugin/protocol/protocol.dart'; -class PluginIsolateChannel implements PluginCommunicationChannel { +/// A channel used to communicate with the analyzer server using the +/// analyzer_plugin protocol +/// +/// Imported from package:analyzer_plugin +class ClientIsolateChannel implements PluginCommunicationChannel { /// Initialize a newly created channel to communicate with the server. - PluginIsolateChannel(this._sendPort) { + ClientIsolateChannel(this._sendPort) { _receivePort = ReceivePort(); _sendPort.send(_receivePort.sendPort); } @@ -22,12 +22,13 @@ class PluginIsolateChannel implements PluginCommunicationChannel { late final ReceivePort _receivePort; /// The subscription that needs to be cancelled when the channel is closed. - StreamSubscription? _subscription; + StreamSubscription? _subscription; @override void close() { - _subscription?.cancel(); + unawaited(_subscription?.cancel()); _subscription = null; + _receivePort.close(); } @override diff --git a/packages/custom_lint/lib/src/log.dart b/packages/custom_lint/lib/src/log.dart deleted file mode 100644 index 86281de2..00000000 --- a/packages/custom_lint/lib/src/log.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -@deprecated -final file = File( - '/Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/log.txt', -); - -void log(Object obj) { - file.writeAsStringSync('\n${DateTime.now()} $obj', mode: FileMode.append); -} diff --git a/packages/custom_lint/lib/src/output/default_output_format.dart b/packages/custom_lint/lib/src/output/default_output_format.dart new file mode 100644 index 00000000..1dc48c6c --- /dev/null +++ b/packages/custom_lint/lib/src/output/default_output_format.dart @@ -0,0 +1,31 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +import 'output_format.dart'; +import 'render_lints.dart'; + +/// The default output format. +class DefaultOutputFormat implements OutputFormat { + @override + void render({ + required Iterable errors, + required Logger log, + }) { + if (errors.isEmpty) { + log.stdout('No issues found!'); + return; + } + + for (final error in errors) { + log.stdout( + ' ${error.location.relativePath}:${error.location.startLine}:${error.location.startColumn}' + ' • ${error.message} • ${error.code} • ${error.severity.name}', + ); + } + + // Display a summary separated from the lints + log.stdout(''); + final errorCount = errors.length; + log.stdout('$errorCount issue${errorCount > 1 ? 's' : ''} found.'); + } +} diff --git a/packages/custom_lint/lib/src/output/json_output_format.dart b/packages/custom_lint/lib/src/output/json_output_format.dart new file mode 100644 index 00000000..11d7359f --- /dev/null +++ b/packages/custom_lint/lib/src/output/json_output_format.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +import 'output_format.dart'; + +/// The JSON output format. +/// +/// Code is an adaption of the original Dart SDK JSON format. +/// See: https://github.com/dart-lang/sdk/blob/main/pkg/dartdev/lib/src/commands/analyze.dart +class JsonOutputFormat implements OutputFormat { + @override + void render({ + required Iterable errors, + required Logger log, + }) { + final diagnostics = >[]; + for (final error in errors) { + final contextMessages = >[]; + if (error.contextMessages != null) { + for (final contextMessage in error.contextMessages!) { + final startOffset = contextMessage.location.offset; + contextMessages.add({ + 'location': _location( + file: contextMessage.location.file, + range: _range( + start: _position( + offset: startOffset, + line: contextMessage.location.startLine, + column: contextMessage.location.startColumn, + ), + end: _position( + offset: startOffset + contextMessage.location.length, + line: contextMessage.location.endLine, + column: contextMessage.location.endColumn, + ), + ), + ), + 'message': contextMessage.message, + }); + } + } + final startOffset = error.location.offset; + diagnostics.add({ + 'code': error.code, + 'severity': error.severity, + 'type': error.type, + 'location': _location( + file: error.location.file, + range: _range( + start: _position( + offset: startOffset, + line: error.location.startLine, + column: error.location.startColumn, + ), + end: _position( + offset: startOffset + error.location.length, + line: error.location.endLine, + column: error.location.endColumn, + ), + ), + ), + 'problemMessage': error.message, + if (error.correction != null) 'correctionMessage': error.correction, + if (contextMessages.isNotEmpty) 'contextMessages': contextMessages, + if (error.url != null) 'documentation': error.url, + }); + } + log.stdout( + json.encode({ + 'version': 1, + 'diagnostics': diagnostics, + }), + ); + } + + Map _location({ + required String file, + required Map range, + }) { + return { + 'file': file, + 'range': range, + }; + } + + Map _position({ + int? offset, + int? line, + int? column, + }) { + return { + 'offset': offset, + 'line': line, + 'column': column, + }; + } + + Map _range({ + required Map start, + required Map end, + }) { + return { + 'start': start, + 'end': end, + }; + } +} diff --git a/packages/custom_lint/lib/src/output/output_format.dart b/packages/custom_lint/lib/src/output/output_format.dart new file mode 100644 index 00000000..fe661462 --- /dev/null +++ b/packages/custom_lint/lib/src/output/output_format.dart @@ -0,0 +1,35 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +/// An enum for the output format. +enum OutputFormatEnum { + /// The default output format. + plain._('default'), + + /// Dart SDK like JSON output format. + json._('json'); + + const OutputFormatEnum._(this.name); + + /// The name of the format. + final String name; + + /// Returns the [OutputFormatEnum] for the given [name]. + static OutputFormatEnum fromName(String name) { + for (final format in OutputFormatEnum.values) { + if (format.name == name) { + return format; + } + } + return plain; + } +} + +/// An abstract class for outputting lints +abstract class OutputFormat { + /// Renders lints according to the format and flags. + void render({ + required Iterable errors, + required Logger log, + }); +} diff --git a/packages/custom_lint/lib/src/output/render_lints.dart b/packages/custom_lint/lib/src/output/render_lints.dart new file mode 100644 index 00000000..78bf4e7e --- /dev/null +++ b/packages/custom_lint/lib/src/output/render_lints.dart @@ -0,0 +1,94 @@ +import 'dart:io'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:cli_util/cli_logging.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; + +import 'default_output_format.dart'; +import 'json_output_format.dart'; +import 'output_format.dart'; + +/// Renders lints according to the given format and flags. +void renderLints( + List lints, { + required Logger log, + required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, + required OutputFormatEnum format, + Progress? progress, +}) { + final OutputFormat outputFormat; + switch (format) { + case OutputFormatEnum.json: + outputFormat = JsonOutputFormat(); + case OutputFormatEnum.plain: + // ignore: unreachable_switch_default, disable for now as won't work on stable + default: + outputFormat = DefaultOutputFormat(); + } + + var errors = lints.expand((lint) => lint.errors); + + var fatal = false; + for (final error in errors) { + error.location.relativePath = p.relative( + error.location.file, + from: workingDirectory.absolute.path, + ); + fatal = fatal || + error.severity == AnalysisErrorSeverity.ERROR || + (fatalWarnings && error.severity == AnalysisErrorSeverity.WARNING) || + (fatalInfos && error.severity == AnalysisErrorSeverity.INFO); + } + + // Sort errors by severity, file, line, column, code, message + // if the output format requires it + errors = errors.sorted((a, b) { + final severityCompare = -AnalysisErrorSeverity.values + .indexOf(a.severity) + .compareTo(AnalysisErrorSeverity.values.indexOf(b.severity)); + if (severityCompare != 0) return severityCompare; + + final fileCompare = + a.location.relativePath.compareTo(b.location.relativePath); + if (fileCompare != 0) return fileCompare; + + final lineCompare = a.location.startLine.compareTo(b.location.startLine); + if (lineCompare != 0) return lineCompare; + + final columnCompare = + a.location.startColumn.compareTo(b.location.startColumn); + if (columnCompare != 0) return columnCompare; + + final codeCompare = a.code.compareTo(b.code); + if (codeCompare != 0) return codeCompare; + + return a.message.compareTo(b.message); + }); + + // Finish progress and display duration (only when ANSI is supported) + progress?.finish(showTiming: true); + + outputFormat.render( + errors: errors, + log: log, + ); + + if (fatal) { + exitCode = 1; + return; + } +} + +final _locationRelativePath = Expando('locationRelativePath'); + +/// A helper extension to set/get +/// the working directory relative path of a [Location]. +extension LocationRelativePath on Location { + /// The working directory relative path of this [Location]. + String get relativePath => _locationRelativePath[this]! as String; + + set relativePath(String path) => _locationRelativePath[this] = path; +} diff --git a/packages/custom_lint/lib/src/plugin_delegate.dart b/packages/custom_lint/lib/src/plugin_delegate.dart new file mode 100644 index 00000000..c3ab4f64 --- /dev/null +++ b/packages/custom_lint/lib/src/plugin_delegate.dart @@ -0,0 +1,296 @@ +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart'; + +import 'v2/custom_lint_analyzer_plugin.dart'; + +/// A delegate for handling certain events based on the platform +abstract class CustomLintDelegate { + /// The server threw an error + void serverError( + CustomLintServer serverPlugin, + Object error, + StackTrace stackTrace, { + required List? allContextRoots, + }); + + /// A plugin failed to start + void pluginInitializationFail( + CustomLintServer serverPlugin, + String message, { + required List? allContextRoots, + }); + + /// The server emitted a message + void serverMessage( + CustomLintServer serverPlugin, + String message, { + required List? allContextRoots, + }); + + /// A plugin emitted a message + void pluginMessage( + CustomLintServer serverPlugin, + String message, { + required String? pluginName, + required List? pluginContextRoots, + }); + + /// A plugin threw outside of a request + void pluginError( + CustomLintServer serverPlugin, + String err, { + required String? stackTrace, + required String pluginName, + required List? pluginContextRoots, + }); + + /// A plugin threw during a request + void requestError( + CustomLintServer serverPlugin, + Request request, + RequestError requestError, { + required List? allContextRoots, + }); +} + +/// Sends the output of some events into a log file +mixin LogCustomLintDelegate implements CustomLintDelegate { + void _log( + List? contextRoots, + String message, { + required String? pluginName, + }) { + // We unfortunately can't log without a context root. + // Hopefully if ran in the CLI, other logging methods will be available. + if (contextRoots == null) return; + + final label = pluginName != null + ? '[$pluginName] ${DateTime.now().toIso8601String()}' + : ''; + + final msg = label.isEmpty + ? message + : message + .split('\n') + .map((e) => e.isEmpty ? '$label\n' : '$label $e\n') + .join(); + + for (final contextRoot in contextRoots) { + final file = File(join(contextRoot.root, 'custom_lint.log')); + + file + ..createSync(recursive: true) + ..writeAsStringSync(msg, mode: FileMode.append); + } + } + + @override + void serverError( + CustomLintServer serverPlugin, + Object error, + StackTrace stackTrace, { + required List? allContextRoots, + }) { + _log(allContextRoots, '$error\n$stackTrace', pluginName: 'custom_lint'); + } + + @override + void pluginInitializationFail( + CustomLintServer serverPlugin, + String message, { + required List? allContextRoots, + }) { + _log(allContextRoots, message, pluginName: null); + } + + @override + @mustCallSuper + void serverMessage( + CustomLintServer serverPlugin, + String message, { + required List? allContextRoots, + }) { + _log( + allContextRoots, + message, + pluginName: null, + ); + } + + @override + @mustCallSuper + void pluginMessage( + CustomLintServer serverPlugin, + String message, { + required String? pluginName, + required List? pluginContextRoots, + }) { + _log( + pluginContextRoots, + message, + pluginName: pluginName, + ); + } + + @override + void requestError( + CustomLintServer serverPlugin, + Request request, + RequestError requestError, { + required List? allContextRoots, + }) { + _log( + allContextRoots, + pluginName: null, + ''' +The request ${request.method} failed with the following error: +${requestError.code} +${requestError.message} +at: +${requestError.stackTrace} +''', + ); + } + + @override + void pluginError( + CustomLintServer serverPlugin, + String err, { + required String? stackTrace, + required String pluginName, + required List? pluginContextRoots, + }) { + if (stackTrace != null) { + _log( + pluginContextRoots, + '$err\n$stackTrace', + pluginName: pluginName, + ); + } else { + _log( + pluginContextRoots, + err, + pluginName: pluginName, + ); + } + } +} + +/// Redirects events to the analyzer server +class AnalyzerPluginCustomLintDelegate + with LogCustomLintDelegate + implements CustomLintDelegate {} + +/// Maps events to the console +class CommandCustomLintDelegate + with LogCustomLintDelegate + implements CustomLintDelegate { + @override + void pluginMessage( + CustomLintServer serverPlugin, + String message, { + required String? pluginName, + required List? pluginContextRoots, + }) { + super.pluginMessage( + serverPlugin, + message, + pluginName: pluginName, + pluginContextRoots: pluginContextRoots, + ); + + final label = pluginName == null ? '' : '[$pluginName]'; + + final msg = label.isEmpty + ? message + : message + .split('\n') + .map((e) => e.isEmpty ? '$label\n' : '$label $e\n') + .join(); + + stdout.write(msg); + } + + @override + void serverError( + CustomLintServer serverPlugin, + Object error, + StackTrace stackTrace, { + required List? allContextRoots, + }) { + exitCode = 1; + super.serverError( + serverPlugin, + error, + stackTrace, + allContextRoots: allContextRoots, + ); + stderr.writeln('$error\n$stackTrace'); + } + + @override + void pluginInitializationFail( + CustomLintServer serverPlugin, + String message, { + required List? allContextRoots, + }) { + exitCode = 1; + super.pluginInitializationFail( + serverPlugin, + message, + allContextRoots: allContextRoots, + ); + stderr.writeln(message); + } + + @override + void pluginError( + CustomLintServer serverPlugin, + String err, { + required String? stackTrace, + required String pluginName, + required List? pluginContextRoots, + }) { + exitCode = 1; + super.pluginError( + serverPlugin, + err, + stackTrace: stackTrace, + pluginName: pluginName, + pluginContextRoots: pluginContextRoots, + ); + if (stackTrace != null) { + stderr.writeln('$err\n$stackTrace'); + } else { + stderr.writeln(err); + } + } + + @override + void requestError( + CustomLintServer serverPlugin, + Request request, + RequestError requestError, { + required List? allContextRoots, + }) { + exitCode = 1; + super.requestError( + serverPlugin, + request, + requestError, + allContextRoots: allContextRoots, + ); + stderr.writeln( + ''' +The request ${request.method} failed with the following error: +${requestError.code} +${requestError.message} +at: +${requestError.stackTrace}''', + ); + } +} diff --git a/packages/custom_lint/lib/src/request_extension.dart b/packages/custom_lint/lib/src/request_extension.dart new file mode 100644 index 00000000..e3016fe5 --- /dev/null +++ b/packages/custom_lint/lib/src/request_extension.dart @@ -0,0 +1,199 @@ +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_constants.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; + +/// Handle an 'analysis.getNavigation' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisGetNavigation = R Function( + AnalysisGetNavigationParams parameters, +); + +/// Handle an 'analysis.handleWatchEvents' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisHandleWatchEvents = R Function( + AnalysisHandleWatchEventsParams parameters, +); + +/// Handle an 'analysis.setContextRoots' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisSetContextRoots = R Function( + AnalysisSetContextRootsParams parameters, +); + +/// Handle an 'analysis.setPriorityFiles' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisSetPriorityFiles = R Function( + AnalysisSetPriorityFilesParams parameters, +); + +/// Handle an 'analysis.setSubscriptions' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisSetSubscriptions = R Function( + AnalysisSetSubscriptionsParams parameters, +); + +/// Handle an 'analysis.updateContent' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleAnalysisUpdateContent = R Function( + AnalysisUpdateContentParams parameters, +); + +/// Handle a 'completion.getSuggestions' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleCompletionGetSuggestions = R Function( + CompletionGetSuggestionsParams parameters, +); + +/// Handle an 'edit.getAssists' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleEditGetAssists = R Function( + EditGetAssistsParams parameters, +); + +/// Handle an 'edit.getAvailableRefactorings' request. Subclasses that override +/// this method in order to participate in refactorings must also override the +/// method [HandleEditGetRefactoring]. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleEditGetAvailableRefactorings = R Function( + EditGetAvailableRefactoringsParams parameters, +); + +/// Handle an 'edit.getFixes' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleEditGetFixes = R Function( + EditGetFixesParams parameters, +); + +/// Handle an 'edit.getRefactoring' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandleEditGetRefactoring = R Function( + EditGetRefactoringParams parameters, +); + +/// Handle a 'plugin.shutdown' request. Subclasses can override this method to +/// perform clean-up, but cannot prevent the plugin from shutting +/// down. +/// +/// Throw a if the request could not be handled. +typedef HandlePluginShutdown = R Function(); + +/// Handle a 'plugin.versionCheck' request. +/// +/// Throw a [RequestFailure] if the request could not be handled. +typedef HandlePluginVersionCheck = R Function( + PluginVersionCheckParams parameters, +); + +/// A channel used to communicate with the analyzer server using the +/// analyzer_plugin protocol +extension RequestX on Request { + /// Subscribes to requests from the analyzer server + R when({ + HandleAnalysisGetNavigation? handleAnalysisGetNavigation, + HandleAnalysisHandleWatchEvents? handleAnalysisHandleWatchEvents, + HandleAnalysisSetContextRoots? handleAnalysisSetContextRoots, + HandleAnalysisSetPriorityFiles? handleAnalysisSetPriorityFiles, + HandleAnalysisSetSubscriptions? handleAnalysisSetSubscriptions, + HandleAnalysisUpdateContent? handleAnalysisUpdateContent, + HandleCompletionGetSuggestions? handleCompletionGetSuggestions, + HandleEditGetAssists? handleEditGetAssists, + HandleEditGetAvailableRefactorings? handleEditGetAvailableRefactorings, + HandleEditGetFixes? handleEditGetFixes, + HandleEditGetRefactoring? handleEditGetRefactoring, + HandlePluginVersionCheck? handlePluginVersionCheck, + HandlePluginShutdown? handlePluginShutdown, + required R Function() orElse, + }) { + switch (method) { + case ANALYSIS_REQUEST_GET_NAVIGATION: + if (handleAnalysisGetNavigation != null) { + final params = AnalysisGetNavigationParams.fromRequest(this); + return handleAnalysisGetNavigation(params); + } + + case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: + if (handleAnalysisHandleWatchEvents != null) { + final params = AnalysisHandleWatchEventsParams.fromRequest(this); + return handleAnalysisHandleWatchEvents(params); + } + + case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: + if (handleAnalysisSetContextRoots != null) { + final params = AnalysisSetContextRootsParams.fromRequest(this); + return handleAnalysisSetContextRoots(params); + } + + case ANALYSIS_REQUEST_SET_PRIORITY_FILES: + if (handleAnalysisSetPriorityFiles != null) { + final params = AnalysisSetPriorityFilesParams.fromRequest(this); + return handleAnalysisSetPriorityFiles(params); + } + + case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS: + if (handleAnalysisSetSubscriptions != null) { + final params = AnalysisSetSubscriptionsParams.fromRequest(this); + return handleAnalysisSetSubscriptions(params); + } + + case ANALYSIS_REQUEST_UPDATE_CONTENT: + if (handleAnalysisUpdateContent != null) { + final params = AnalysisUpdateContentParams.fromRequest(this); + return handleAnalysisUpdateContent(params); + } + + case COMPLETION_REQUEST_GET_SUGGESTIONS: + if (handleCompletionGetSuggestions != null) { + final params = CompletionGetSuggestionsParams.fromRequest(this); + return handleCompletionGetSuggestions(params); + } + + case EDIT_REQUEST_GET_ASSISTS: + if (handleEditGetAssists != null) { + final params = EditGetAssistsParams.fromRequest(this); + return handleEditGetAssists(params); + } + + case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS: + final params = EditGetAvailableRefactoringsParams.fromRequest(this); + if (handleEditGetAvailableRefactorings != null) { + return handleEditGetAvailableRefactorings(params); + } + + case EDIT_REQUEST_GET_FIXES: + if (handleEditGetFixes != null) { + final params = EditGetFixesParams.fromRequest(this); + return handleEditGetFixes(params); + } + + case EDIT_REQUEST_GET_REFACTORING: + if (handleEditGetRefactoring != null) { + final params = EditGetRefactoringParams.fromRequest(this); + return handleEditGetRefactoring(params); + } + + case PLUGIN_REQUEST_SHUTDOWN: + if (handlePluginShutdown != null) { + return handlePluginShutdown(); + } + + case PLUGIN_REQUEST_VERSION_CHECK: + final params = PluginVersionCheckParams.fromRequest(this); + if (handlePluginVersionCheck != null) { + return handlePluginVersionCheck(params); + } + } + + return orElse(); + } +} diff --git a/packages/custom_lint/lib/src/runner.dart b/packages/custom_lint/lib/src/runner.dart new file mode 100644 index 00000000..a20940a1 --- /dev/null +++ b/packages/custom_lint/lib/src/runner.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:cli_util/cli_util.dart'; + +import 'server_isolate_channel.dart'; +import 'v2/custom_lint_analyzer_plugin.dart'; +import 'workspace.dart'; + +const _analyzerPluginProtocolVersion = '1.0.0-alpha.0'; + +/// A runner for programmatically interacting with a plugin. +class CustomLintRunner { + /// A runner for programmatically interacting with a plugin. + CustomLintRunner(this._server, this.workspace, this.channel); + + /// The custom_lint project that is being run. + final CustomLintWorkspace workspace; + + /// The connection between the server and the plugin. + final ServerIsolateChannel channel; + final CustomLintServer _server; + final _accumulatedLints = {}; + StreamSubscription? _lintSubscription; + + var _closed = false; + + /// Starts the plugin and sends the necessary requests for initializing it. + late final initialize = Future(() async { + _lintSubscription = channel.lints.listen((event) { + _accumulatedLints[event.file] = event; + }); + + await channel.sendRequest( + PluginVersionCheckParams( + '', + sdkPath, + _analyzerPluginProtocolVersion, + ), + ); + await channel.sendRequest( + AnalysisSetContextRootsParams(workspace.contextRoots), + ); + }); + + /// Obtains the list of lints for the current workspace. + Future> getLints({required bool reload}) async { + if (reload) _accumulatedLints.clear(); + + await _server.awaitAnalysisDone(reload: reload); + + return _accumulatedLints.values.toList() + ..sort((a, b) => a.file.compareTo(b.file)); + } + + /// Obtains the list of fixes for a given file/offset combo + Future getFixes( + String path, + int offset, + ) async { + final result = await channel.sendRequest( + EditGetFixesParams(path, offset), + ); + return EditGetFixesResult.fromResponse(result); + } + + /// Stop the command runner, sending a [PluginShutdownParams] request in the process. + Future close() async { + if (_closed) return; + _closed = true; + + try { + await channel.sendRequest(PluginShutdownParams()); + } finally { + await _lintSubscription?.cancel(); + } + } +} diff --git a/packages/custom_lint/lib/src/server_isolate_channel.dart b/packages/custom_lint/lib/src/server_isolate_channel.dart new file mode 100644 index 00000000..09c9598a --- /dev/null +++ b/packages/custom_lint/lib/src/server_isolate_channel.dart @@ -0,0 +1,160 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_constants.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +// ignore: implementation_imports, not exported +import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' + show RequestParams; +import 'package:async/async.dart'; +import 'package:uuid/uuid.dart'; + +import 'async_operation.dart'; + +const _uuid = Uuid(); + +/// A base class for the protocol responsible with interacting with using the +/// analyzer_plugin API +mixin ChannelBase { + /// The stream containing any message from the client + Stream get inputStream; + + /// The [Notification]s emitted by the plugin + late final Stream notifications = inputStream + .where((e) => e is Map) + .map((e) => e! as Map) + .where((e) => e.containsKey(Notification.EVENT)) + .map(Notification.fromJson); + + /// The [Response]s emitted by the plugin + late final Stream responses = inputStream + .where((event) => event is Map) + .map((event) => event! as Map) + .where((e) => e.containsKey(Response.ID)) + .map(Response.fromJson); + + /// Error [Notification]s. + late final Stream pluginErrors = + StreamGroup.mergeBroadcast([ + // Manual error notifications from the plugin + notifications + .where((e) => e.event == PLUGIN_NOTIFICATION_ERROR) + .map(PluginErrorParams.fromNotification), + + // When the receivePort is passed to Isolate.onError, error events are + // received as ["error", "stackTrace"] + inputStream + .where((event) => event is List) + .cast>() + .map((event) { + final error = event.first.toString(); + final stackTrace = event.last.toString(); + return PluginErrorParams(false, error, stackTrace); + }), + ]); + + /// Errors for [Request]s that failed. + late final Stream responseErrors = + responses.where((e) => e.error != null).map((e) => e.error!); + + /// Sends a json object to the plugin, without awaiting for an answer + Future sendJson(Map json); + + /// Send a request and obtains the associated response + Future sendRequest( + RequestParams requestParams, + ) async { + final id = _uuid.v4(); + final request = requestParams.toRequest(id); + final responseFuture = responses.firstWhere( + (message) { + return message.id == id; + }, + orElse: () => throw StateError( + 'No response for request ${request.method} $id', + ), + ); + await sendJson(request.toJson()); + final response = await responseFuture; + + if (response.error != null) { + throw _PrettyRequestFailure(response.error!); + } + + return response; + } + + /// Send a request and obtains the associated response + Future sendRequestParams( + RequestParams requestParams, + ) async { + final id = _uuid.v4(); + + final request = requestParams.toRequest(id); + final responseFuture = responses.firstWhere( + (message) => message.id == id, + orElse: () => throw StateError( + 'No response for request ${request.method} $id', + ), + ); + await sendJson(request.toJson()); + final response = await responseFuture; + + if (response.error != null) { + throw _PrettyRequestFailure(response.error!); + } + + return response; + } +} + +class _PrettyRequestFailure extends RequestFailure { + _PrettyRequestFailure(super.error); + + @override + String toString() { + return '_PrettyRequestFailure: $error'; + } +} + +/// Mixin for Isolate-based channels +abstract class IsolateChannelBase with ChannelBase { + /// Mixin for Isolate-based channels + IsolateChannelBase(this.receivePort) { + _sendPort = inputStream + .where((event) => event is SendPort) + .cast() + .safeFirst; + } + + /// The [ReceivePort] responsible for listening to requests. + final ReceivePort receivePort; + + @override + late final Stream inputStream = receivePort.asBroadcastStream(); + + /// The [SendPort] responsible for sending events to the isolate. + late final Future _sendPort; + + @override + Future sendJson(Map json) { + return _sendPort.then((value) => value.send(json)); + } +} + +/// An interface for interacting with the plugin server. +class ServerIsolateChannel extends IsolateChannelBase { + /// An interface for interacting with the plugin server. + ServerIsolateChannel() : super(ReceivePort()); + + /// Lints emitted by the plugin + late final Stream lints = notifications + .where((e) => e.event == ANALYSIS_NOTIFICATION_ERRORS) + .map(AnalysisErrorsParams.fromNotification); + + /// Releases the associated resources. + Future close() async { + receivePort.close(); + } +} diff --git a/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart b/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart new file mode 100644 index 00000000..5c49b8ff --- /dev/null +++ b/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart @@ -0,0 +1,439 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_constants.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +// ignore: implementation_imports, not exported +import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' + show ResponseResult; +import 'package:async/async.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:uuid/uuid.dart'; + +import '../async_operation.dart'; +import '../channels.dart'; +import '../plugin_delegate.dart'; +import '../request_extension.dart'; +import 'protocol.dart'; +import 'server_to_client_channel.dart'; + +/// The custom_lint server, in charge of interacting with analyzer_plugin +/// and starting custom_lint plugins +class CustomLintServer { + CustomLintServer._({ + required this.watchMode, + required this.includeBuiltInLints, + required this.delegate, + required this.workingDirectory, + required this.fix, + }); + + /// Start the server while also capturing prints and errors. + /// + /// Logic after the [start] should be wrapped in a [runZoned] to make sure + /// errors and prints continue to be captured. + static Future start({ + required SendPort sendPort, + required bool? watchMode, + required bool includeBuiltInLints, + required bool fix, + required CustomLintDelegate delegate, + required Directory workingDirectory, + }) { + late CustomLintServer server; + + return runZoned( + () => server, + () { + server = CustomLintServer._( + watchMode: watchMode, + fix: fix, + includeBuiltInLints: includeBuiltInLints, + delegate: delegate, + workingDirectory: workingDirectory, + ); + server._start(sendPort); + + return server; + }, + ); + } + + /// Run the given [body] in a zone that captures errors and prints and + /// sends them to the server for handling. + /// + /// Do not close the server within [runZoned], as this could cause a race condition + /// on errors/prints handling, where an error/print happens after the server is closed, + /// causing the event to be silenced. + static Future runZoned( + CustomLintServer Function() server, + FutureOr Function() body, + ) { + return asyncRunZonedGuarded( + () => body(), + (err, stack) { + server().handleUncaughtError(err, stack); + }, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) { + server().handlePrint( + line, + isClientMessage: false, + ); + }, + ), + ); + } + + /// The directory in which the server is running. + final Directory workingDirectory; + + /// The object in charge of logging events and possibly rendering events + /// in the console (if ran from a terminal). + final CustomLintDelegate delegate; + + /// The interface for discussing with analyzer_plugin + late final AnalyzerPluginClientChannel _analyzerPluginClientChannel; + + /// Whether plugins should be started in watch mode + final bool? watchMode; + + /// If enabled, attempt to fix all issues found before reporting them. + /// Can only be enabled in the CLI. + final bool fix; + + /// Whether plugins should include lints used for debugging. + final bool includeBuiltInLints; + + late final StreamSubscription _requestSubscription; + StreamSubscription? _clientChannelEventsSubscription; + late PluginVersionCheckParams _pluginVersionCheckParams; + + final _clientChannel = + BehaviorSubject(); + final _contextRoots = BehaviorSubject(); + final _runner = PendingOperation(); + + /// A shorthand for accessing the current list of context roots. + Future?> get _allContextRoots { + return _contextRoots.firstOrNull.then((value) => value?.roots); + } + + void _start(SendPort sendPort) { + _analyzerPluginClientChannel = JsonSendPortChannel(sendPort); + _requestSubscription = _analyzerPluginClientChannel.messages + .map((e) => e! as Map) + .map(Request.fromJson) + .listen(_handleRequest); + } + + /// Waits for the plugins to complete their analysis + Future awaitAnalysisDone({ + required bool reload, + }) => + _runner.run(() async { + final clientChannel = await _clientChannel.safeFirst; + if (clientChannel == null) return; + + await clientChannel.sendCustomLintRequest( + CustomLintRequest.awaitAnalysisDone( + id: const Uuid().v4(), + reload: reload, + ), + ); + + // Pinging the client to flush events. This should ensure notifications are handled + await clientChannel.sendCustomLintRequest( + CustomLintRequest.ping(id: const Uuid().v4()), + ); + }); + + Future _handleRequest(Request request) async { + final requestTime = DateTime.now().millisecondsSinceEpoch; + Future sendResponse({ + ResponseResult? data, + RequestError? error, + }) async { + _analyzerPluginClientChannel.sendResponse( + requestID: request.id, + requestTime: requestTime, + data: data, + error: error, + ); + } + + try { + final result = await request.when>( + handlePluginVersionCheck: _handlePluginVersionCheck, + handleAnalysisSetContextRoots: _handleAnalysisSetContextRoots, + handlePluginShutdown: () async { + try { + await sendResponse(data: PluginShutdownResult()); + return null; + } finally { + await close(); + } + }, + orElse: () async { + return _runner.run(() async { + final clientChannel = await _clientChannel.safeFirst; + if (clientChannel == null) return null; + + final response = + await clientChannel.sendAnalyzerPluginRequest(request); + _analyzerPluginClientChannel.sendJson(response.toJson()); + return null; + }); + }, + ); + + /// A response was already sent, so nothing to do. + if (result == null) return; + + await sendResponse(data: result); + } catch (err, stack) { + await sendResponse( + error: RequestError( + RequestErrorCode.PLUGIN_ERROR, + err.toString(), + stackTrace: stack.toString(), + ), + ); + delegate.requestError( + this, + request, + RequestError( + RequestErrorCode.PLUGIN_ERROR, + err.toString(), + stackTrace: stack.toString(), + ), + allContextRoots: + await _contextRoots.safeFirst.then((value) => value.roots), + ); + } + } + + /// An uncaught error was detected (unrelated to requests). + /// Logging the error and notifying the analyzer server + Future handleUncaughtError(Object error, StackTrace stackTrace) => + _runner.run(() async { + _analyzerPluginClientChannel.sendJson( + PluginErrorParams(false, error.toString(), stackTrace.toString()) + .toNotification() + .toJson(), + ); + + delegate.serverError( + this, + error, + stackTrace, + allContextRoots: await _allContextRoots, + ); + }); + + /// A life-cycle for when the server failed to start the plugins. + Future handlePluginInitializationFail() => _runner.run(() async { + final contextRoots = await _allContextRoots; + + delegate.pluginInitializationFail( + this, + 'Failed to start plugins', + allContextRoots: contextRoots, + ); + + _analyzerPluginClientChannel.sendJson( + PluginErrorParams(true, 'Failed to start plugins', '') + .toNotification() + .toJson(), + ); + }); + + /// A print was detected. This will redirect it to a log file. + Future handlePrint( + String message, { + required bool isClientMessage, + }) => + _runner.run(() async { + final roots = await _contextRoots.safeFirst; + + if (!isClientMessage) { + delegate.serverMessage( + this, + '$message\n', + allContextRoots: roots.roots, + ); + } else { + delegate.pluginMessage( + this, + message, + pluginName: null, + pluginContextRoots: roots.roots, + ); + } + }); + + Future? _closeFuture; + + /// Stops the server, closing all channels. + Future close() async { + // Already stopped the server before. No need to run things again. + if (_closeFuture != null) return _closeFuture; + + return _closeFuture = Future(() async { + // Cancel pending operations + await _contextRoots.close(); + + // Flushes logs before stopping server. + await _runner.wait(); + + try { + await Future.wait([ + _clientChannel.safeFirst + .then((clientChannel) => clientChannel?.close()), + _clientChannel.close(), + _requestSubscription.cancel(), + if (_clientChannelEventsSubscription != null) + _clientChannelEventsSubscription!.cancel(), + ]) + // Close the connection after previous disposals are done, to make sure + // the shutdown request (if any) receives a response + .whenComplete(_analyzerPluginClientChannel.close); + } finally { + // Wait for remaining operations to complete + await _runner.wait(); + } + }) + // Make sure "close" never throws, so that follow-up dispose logic can continue. + .catchError((_) {}); + } + + PluginVersionCheckResult _handlePluginVersionCheck( + PluginVersionCheckParams parameters, + ) { + _pluginVersionCheckParams = parameters; + + final versionString = parameters.version; + final serverVersion = Version.parse(versionString); + final clientVersion = Version.parse('1.0.0-alpha.0'); + + return PluginVersionCheckResult( + serverVersion <= clientVersion, + 'custom_lint', + clientVersion.toString(), + ['*'], + contactInfo: 'https://github.com/invertase/dart_custom_lint/issues', + ); + } + + Future _handleAnalysisSetContextRoots( + AnalysisSetContextRootsParams parameters, + ) => + _runner.run(() async { + _contextRoots.add(parameters); + + await _maybeSpawnCustomLintPlugin(parameters); + + return AnalysisSetContextRootsResult(); + }); + + Future _maybeSpawnCustomLintPlugin( + AnalysisSetContextRootsParams parameters, + ) async { + // "setContextRoots" is always called after "pluginVersionCheck", so we can + // safely assume that the version check parameters are set. + + if (_clientChannel.hasValue) { + await _clientChannel.value?.setContextRoots(parameters); + return; + } + + SocketCustomLintServerToClientChannel? clientChannel; + + try { + clientChannel = await SocketCustomLintServerToClientChannel.create( + this, + _pluginVersionCheckParams, + parameters, + workingDirectory: workingDirectory, + ); + _clientChannel.add(clientChannel); + if (clientChannel == null) return; + } catch (err, stack) { + _clientChannel.addError(err, stack); + rethrow; + } + + // Listening to event before init, to make sure messages during the init are handled. + _clientChannelEventsSubscription = clientChannel.events.listen( + _handleEvent, + ); + + final configs = await Future.wait( + parameters.roots.map( + (e) async { + final packageConfig = await findPackageConfig(Directory(e.root)); + if (packageConfig == null) return null; + + return CustomLintConfigs.parse( + PhysicalResourceProvider.INSTANCE.getFile( + p.join(e.root, 'analysis_options.yaml'), + ), + packageConfig, + ); + }, + ), + ); + + await clientChannel.init( + debug: configs.any((e) => e != null && e.debug), + ); + } + + Future _handleEvent(CustomLintEvent event) => _runner.run(() async { + switch (event) { + case CustomLintEventAnalyzerPluginNotification(): + _analyzerPluginClientChannel.sendJson(event.notification.toJson()); + + final notification = event.notification; + if (notification.event == PLUGIN_NOTIFICATION_ERROR) { + final error = PluginErrorParams.fromNotification(notification); + _analyzerPluginClientChannel + .sendJson(error.toNotification().toJson()); + delegate.pluginError( + this, + error.message, + stackTrace: error.stackTrace, + pluginName: '', + pluginContextRoots: await _allContextRoots, + ); + } + case CustomLintEventError(): + _analyzerPluginClientChannel.sendJson( + PluginErrorParams(false, event.message, event.stackTrace) + .toNotification() + .toJson(), + ); + delegate.pluginError( + this, + event.message, + stackTrace: event.stackTrace, + pluginName: event.pluginName ?? 'custom_lint client', + pluginContextRoots: await _allContextRoots, + ); + case CustomLintEventPrint(): + delegate.pluginMessage( + this, + event.message, + pluginName: event.pluginName ?? 'custom_lint client', + pluginContextRoots: await _allContextRoots, + ); + } + }); +} diff --git a/packages/custom_lint/lib/src/v2/protocol.dart b/packages/custom_lint/lib/src/v2/protocol.dart new file mode 100644 index 00000000..7771b618 --- /dev/null +++ b/packages/custom_lint/lib/src/v2/protocol.dart @@ -0,0 +1,128 @@ +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'protocol.g.dart'; +part 'protocol.freezed.dart'; + +/// A base class shared between all custom_lint requests +@freezed +sealed class CustomLintRequest with _$CustomLintRequest { + /// A request using the analyzer_plugin protocol + factory CustomLintRequest.analyzerPluginRequest( + Request request, { + required String id, + }) = CustomLintRequestAnalyzerPluginRequest; + + /// Requests to wait for the client to complete its analysis + factory CustomLintRequest.awaitAnalysisDone({ + required String id, + required bool reload, + }) = CustomLintRequestAwaitAnalysisDone; + + /// Sends a meaningless message to the client, waiting for a response. + factory CustomLintRequest.ping({required String id}) = CustomLintRequestPing; + + /// Decode a custom_lint request from JSON + factory CustomLintRequest.fromJson(Map json) => + _$CustomLintRequestFromJson(json); + + /// The unique request ID + @override + String get id; +} + +/// The base class for all responses to a custom_lint request. +@freezed +sealed class CustomLintResponse with _$CustomLintResponse { + /// The response for an analyzer_plugin request + factory CustomLintResponse.analyzerPluginResponse( + Response response, { + required String id, + }) = CustomLintResponseAnalyzerPluginResponse; + + /// The message sent when the client has completed its analysis + factory CustomLintResponse.awaitAnalysisDone({required String id}) = + CustomLintResponseAwaitAnalysisDone; + + /// The reply to a ping request + factory CustomLintResponse.pong({required String id}) = + CustomLintResponsePong; + + /// A request failed + factory CustomLintResponse.error({ + required String id, + required String message, + required String stackTrace, + }) = CustomLintResponseError; + + /// Decode a response from JSON + factory CustomLintResponse.fromJson(Map json) => + _$CustomLintResponseFromJson(json); + + @override + String get id; +} + +/// A base class between all messages from the client, be it request responses, +/// or spontaneous events. +@Freezed(copyWith: false) +abstract class CustomLintMessage with _$CustomLintMessage { + /// A spontaneous event, not associated with a request + factory CustomLintMessage.event(CustomLintEvent event) = + CustomLintMessageEvent; + + /// A response to a request + factory CustomLintMessage.response(CustomLintResponse response) = + CustomLintMessageResponse; + + /// Decode a message from JSONs + factory CustomLintMessage.fromJson(Map json) => + _$CustomLintMessageFromJson(json); +} + +/// A class for decoding a [Notification]. +class NotificationJsonConverter + extends JsonConverter> { + /// A class for decoding a [Notification]. + const NotificationJsonConverter(); + + @override + Notification fromJson(Map json) { + return Notification( + json[Notification.EVENT]! as String, + Map.from(json[Notification.PARAMS]! as Map), + ); + } + + @override + Map toJson(Notification object) { + return object.toJson(); + } +} + +/// A base class for all custom_lint events +@freezed +sealed class CustomLintEvent with _$CustomLintEvent { + /// The client sent a [Notification] using the analyzer_plugin protocol + factory CustomLintEvent.analyzerPluginNotification( + @NotificationJsonConverter() Notification notification, + ) = CustomLintEventAnalyzerPluginNotification; + // TODO add source change event? + + /// A spontaneous error, unrelated to a request + factory CustomLintEvent.error( + String message, + String stackTrace, { + required String? pluginName, + }) = CustomLintEventError; + + /// A log output + factory CustomLintEvent.print( + String message, { + required String? pluginName, + }) = CustomLintEventPrint; + + /// Decode an event from JSON + factory CustomLintEvent.fromJson(Map json) => + _$CustomLintEventFromJson(json); +} diff --git a/packages/custom_lint/lib/src/v2/protocol.freezed.dart b/packages/custom_lint/lib/src/v2/protocol.freezed.dart new file mode 100644 index 00000000..65a3120d --- /dev/null +++ b/packages/custom_lint/lib/src/v2/protocol.freezed.dart @@ -0,0 +1,1232 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'protocol.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +CustomLintRequest _$CustomLintRequestFromJson(Map json) { + switch (json['runtimeType']) { + case 'analyzerPluginRequest': + return CustomLintRequestAnalyzerPluginRequest.fromJson(json); + case 'awaitAnalysisDone': + return CustomLintRequestAwaitAnalysisDone.fromJson(json); + case 'ping': + return CustomLintRequestPing.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'CustomLintRequest', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$CustomLintRequest { + String get id; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintRequestCopyWith get copyWith => + _$CustomLintRequestCopyWithImpl( + this as CustomLintRequest, _$identity); + + /// Serializes this CustomLintRequest to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintRequest && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + @override + String toString() { + return 'CustomLintRequest(id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintRequestCopyWith<$Res> { + factory $CustomLintRequestCopyWith( + CustomLintRequest value, $Res Function(CustomLintRequest) _then) = + _$CustomLintRequestCopyWithImpl; + @useResult + $Res call({String id}); +} + +/// @nodoc +class _$CustomLintRequestCopyWithImpl<$Res> + implements $CustomLintRequestCopyWith<$Res> { + _$CustomLintRequestCopyWithImpl(this._self, this._then); + + final CustomLintRequest _self; + final $Res Function(CustomLintRequest) _then; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + }) { + return _then(_self.copyWith( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintRequestAnalyzerPluginRequest implements CustomLintRequest { + CustomLintRequestAnalyzerPluginRequest(this.request, + {required this.id, final String? $type}) + : $type = $type ?? 'analyzerPluginRequest'; + factory CustomLintRequestAnalyzerPluginRequest.fromJson( + Map json) => + _$CustomLintRequestAnalyzerPluginRequestFromJson(json); + + final Request request; + @override + final String id; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintRequestAnalyzerPluginRequestCopyWith< + CustomLintRequestAnalyzerPluginRequest> + get copyWith => _$CustomLintRequestAnalyzerPluginRequestCopyWithImpl< + CustomLintRequestAnalyzerPluginRequest>(this, _$identity); + + @override + Map toJson() { + return _$CustomLintRequestAnalyzerPluginRequestToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintRequestAnalyzerPluginRequest && + (identical(other.request, request) || other.request == request) && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, request, id); + + @override + String toString() { + return 'CustomLintRequest.analyzerPluginRequest(request: $request, id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintRequestAnalyzerPluginRequestCopyWith<$Res> + implements $CustomLintRequestCopyWith<$Res> { + factory $CustomLintRequestAnalyzerPluginRequestCopyWith( + CustomLintRequestAnalyzerPluginRequest value, + $Res Function(CustomLintRequestAnalyzerPluginRequest) _then) = + _$CustomLintRequestAnalyzerPluginRequestCopyWithImpl; + @override + @useResult + $Res call({Request request, String id}); +} + +/// @nodoc +class _$CustomLintRequestAnalyzerPluginRequestCopyWithImpl<$Res> + implements $CustomLintRequestAnalyzerPluginRequestCopyWith<$Res> { + _$CustomLintRequestAnalyzerPluginRequestCopyWithImpl(this._self, this._then); + + final CustomLintRequestAnalyzerPluginRequest _self; + final $Res Function(CustomLintRequestAnalyzerPluginRequest) _then; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? request = null, + Object? id = null, + }) { + return _then(CustomLintRequestAnalyzerPluginRequest( + null == request + ? _self.request + : request // ignore: cast_nullable_to_non_nullable + as Request, + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintRequestAwaitAnalysisDone implements CustomLintRequest { + CustomLintRequestAwaitAnalysisDone( + {required this.id, required this.reload, final String? $type}) + : $type = $type ?? 'awaitAnalysisDone'; + factory CustomLintRequestAwaitAnalysisDone.fromJson( + Map json) => + _$CustomLintRequestAwaitAnalysisDoneFromJson(json); + + @override + final String id; + final bool reload; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintRequestAwaitAnalysisDoneCopyWith< + CustomLintRequestAwaitAnalysisDone> + get copyWith => _$CustomLintRequestAwaitAnalysisDoneCopyWithImpl< + CustomLintRequestAwaitAnalysisDone>(this, _$identity); + + @override + Map toJson() { + return _$CustomLintRequestAwaitAnalysisDoneToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintRequestAwaitAnalysisDone && + (identical(other.id, id) || other.id == id) && + (identical(other.reload, reload) || other.reload == reload)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, reload); + + @override + String toString() { + return 'CustomLintRequest.awaitAnalysisDone(id: $id, reload: $reload)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintRequestAwaitAnalysisDoneCopyWith<$Res> + implements $CustomLintRequestCopyWith<$Res> { + factory $CustomLintRequestAwaitAnalysisDoneCopyWith( + CustomLintRequestAwaitAnalysisDone value, + $Res Function(CustomLintRequestAwaitAnalysisDone) _then) = + _$CustomLintRequestAwaitAnalysisDoneCopyWithImpl; + @override + @useResult + $Res call({String id, bool reload}); +} + +/// @nodoc +class _$CustomLintRequestAwaitAnalysisDoneCopyWithImpl<$Res> + implements $CustomLintRequestAwaitAnalysisDoneCopyWith<$Res> { + _$CustomLintRequestAwaitAnalysisDoneCopyWithImpl(this._self, this._then); + + final CustomLintRequestAwaitAnalysisDone _self; + final $Res Function(CustomLintRequestAwaitAnalysisDone) _then; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + Object? reload = null, + }) { + return _then(CustomLintRequestAwaitAnalysisDone( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + reload: null == reload + ? _self.reload + : reload // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintRequestPing implements CustomLintRequest { + CustomLintRequestPing({required this.id, final String? $type}) + : $type = $type ?? 'ping'; + factory CustomLintRequestPing.fromJson(Map json) => + _$CustomLintRequestPingFromJson(json); + + @override + final String id; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintRequestPingCopyWith get copyWith => + _$CustomLintRequestPingCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$CustomLintRequestPingToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintRequestPing && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + @override + String toString() { + return 'CustomLintRequest.ping(id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintRequestPingCopyWith<$Res> + implements $CustomLintRequestCopyWith<$Res> { + factory $CustomLintRequestPingCopyWith(CustomLintRequestPing value, + $Res Function(CustomLintRequestPing) _then) = + _$CustomLintRequestPingCopyWithImpl; + @override + @useResult + $Res call({String id}); +} + +/// @nodoc +class _$CustomLintRequestPingCopyWithImpl<$Res> + implements $CustomLintRequestPingCopyWith<$Res> { + _$CustomLintRequestPingCopyWithImpl(this._self, this._then); + + final CustomLintRequestPing _self; + final $Res Function(CustomLintRequestPing) _then; + + /// Create a copy of CustomLintRequest + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + }) { + return _then(CustomLintRequestPing( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +CustomLintResponse _$CustomLintResponseFromJson(Map json) { + switch (json['runtimeType']) { + case 'analyzerPluginResponse': + return CustomLintResponseAnalyzerPluginResponse.fromJson(json); + case 'awaitAnalysisDone': + return CustomLintResponseAwaitAnalysisDone.fromJson(json); + case 'pong': + return CustomLintResponsePong.fromJson(json); + case 'error': + return CustomLintResponseError.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'CustomLintResponse', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$CustomLintResponse { + String get id; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintResponseCopyWith get copyWith => + _$CustomLintResponseCopyWithImpl( + this as CustomLintResponse, _$identity); + + /// Serializes this CustomLintResponse to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintResponse && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + @override + String toString() { + return 'CustomLintResponse(id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintResponseCopyWith<$Res> { + factory $CustomLintResponseCopyWith( + CustomLintResponse value, $Res Function(CustomLintResponse) _then) = + _$CustomLintResponseCopyWithImpl; + @useResult + $Res call({String id}); +} + +/// @nodoc +class _$CustomLintResponseCopyWithImpl<$Res> + implements $CustomLintResponseCopyWith<$Res> { + _$CustomLintResponseCopyWithImpl(this._self, this._then); + + final CustomLintResponse _self; + final $Res Function(CustomLintResponse) _then; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + }) { + return _then(_self.copyWith( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintResponseAnalyzerPluginResponse implements CustomLintResponse { + CustomLintResponseAnalyzerPluginResponse(this.response, + {required this.id, final String? $type}) + : $type = $type ?? 'analyzerPluginResponse'; + factory CustomLintResponseAnalyzerPluginResponse.fromJson( + Map json) => + _$CustomLintResponseAnalyzerPluginResponseFromJson(json); + + final Response response; + @override + final String id; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintResponseAnalyzerPluginResponseCopyWith< + CustomLintResponseAnalyzerPluginResponse> + get copyWith => _$CustomLintResponseAnalyzerPluginResponseCopyWithImpl< + CustomLintResponseAnalyzerPluginResponse>(this, _$identity); + + @override + Map toJson() { + return _$CustomLintResponseAnalyzerPluginResponseToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintResponseAnalyzerPluginResponse && + (identical(other.response, response) || + other.response == response) && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, response, id); + + @override + String toString() { + return 'CustomLintResponse.analyzerPluginResponse(response: $response, id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintResponseAnalyzerPluginResponseCopyWith<$Res> + implements $CustomLintResponseCopyWith<$Res> { + factory $CustomLintResponseAnalyzerPluginResponseCopyWith( + CustomLintResponseAnalyzerPluginResponse value, + $Res Function(CustomLintResponseAnalyzerPluginResponse) _then) = + _$CustomLintResponseAnalyzerPluginResponseCopyWithImpl; + @override + @useResult + $Res call({Response response, String id}); +} + +/// @nodoc +class _$CustomLintResponseAnalyzerPluginResponseCopyWithImpl<$Res> + implements $CustomLintResponseAnalyzerPluginResponseCopyWith<$Res> { + _$CustomLintResponseAnalyzerPluginResponseCopyWithImpl( + this._self, this._then); + + final CustomLintResponseAnalyzerPluginResponse _self; + final $Res Function(CustomLintResponseAnalyzerPluginResponse) _then; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? response = null, + Object? id = null, + }) { + return _then(CustomLintResponseAnalyzerPluginResponse( + null == response + ? _self.response + : response // ignore: cast_nullable_to_non_nullable + as Response, + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintResponseAwaitAnalysisDone implements CustomLintResponse { + CustomLintResponseAwaitAnalysisDone({required this.id, final String? $type}) + : $type = $type ?? 'awaitAnalysisDone'; + factory CustomLintResponseAwaitAnalysisDone.fromJson( + Map json) => + _$CustomLintResponseAwaitAnalysisDoneFromJson(json); + + @override + final String id; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintResponseAwaitAnalysisDoneCopyWith< + CustomLintResponseAwaitAnalysisDone> + get copyWith => _$CustomLintResponseAwaitAnalysisDoneCopyWithImpl< + CustomLintResponseAwaitAnalysisDone>(this, _$identity); + + @override + Map toJson() { + return _$CustomLintResponseAwaitAnalysisDoneToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintResponseAwaitAnalysisDone && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + @override + String toString() { + return 'CustomLintResponse.awaitAnalysisDone(id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintResponseAwaitAnalysisDoneCopyWith<$Res> + implements $CustomLintResponseCopyWith<$Res> { + factory $CustomLintResponseAwaitAnalysisDoneCopyWith( + CustomLintResponseAwaitAnalysisDone value, + $Res Function(CustomLintResponseAwaitAnalysisDone) _then) = + _$CustomLintResponseAwaitAnalysisDoneCopyWithImpl; + @override + @useResult + $Res call({String id}); +} + +/// @nodoc +class _$CustomLintResponseAwaitAnalysisDoneCopyWithImpl<$Res> + implements $CustomLintResponseAwaitAnalysisDoneCopyWith<$Res> { + _$CustomLintResponseAwaitAnalysisDoneCopyWithImpl(this._self, this._then); + + final CustomLintResponseAwaitAnalysisDone _self; + final $Res Function(CustomLintResponseAwaitAnalysisDone) _then; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + }) { + return _then(CustomLintResponseAwaitAnalysisDone( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintResponsePong implements CustomLintResponse { + CustomLintResponsePong({required this.id, final String? $type}) + : $type = $type ?? 'pong'; + factory CustomLintResponsePong.fromJson(Map json) => + _$CustomLintResponsePongFromJson(json); + + @override + final String id; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintResponsePongCopyWith get copyWith => + _$CustomLintResponsePongCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$CustomLintResponsePongToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintResponsePong && + (identical(other.id, id) || other.id == id)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id); + + @override + String toString() { + return 'CustomLintResponse.pong(id: $id)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintResponsePongCopyWith<$Res> + implements $CustomLintResponseCopyWith<$Res> { + factory $CustomLintResponsePongCopyWith(CustomLintResponsePong value, + $Res Function(CustomLintResponsePong) _then) = + _$CustomLintResponsePongCopyWithImpl; + @override + @useResult + $Res call({String id}); +} + +/// @nodoc +class _$CustomLintResponsePongCopyWithImpl<$Res> + implements $CustomLintResponsePongCopyWith<$Res> { + _$CustomLintResponsePongCopyWithImpl(this._self, this._then); + + final CustomLintResponsePong _self; + final $Res Function(CustomLintResponsePong) _then; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + }) { + return _then(CustomLintResponsePong( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintResponseError implements CustomLintResponse { + CustomLintResponseError( + {required this.id, + required this.message, + required this.stackTrace, + final String? $type}) + : $type = $type ?? 'error'; + factory CustomLintResponseError.fromJson(Map json) => + _$CustomLintResponseErrorFromJson(json); + + @override + final String id; + final String message; + final String stackTrace; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintResponseErrorCopyWith get copyWith => + _$CustomLintResponseErrorCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$CustomLintResponseErrorToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintResponseError && + (identical(other.id, id) || other.id == id) && + (identical(other.message, message) || other.message == message) && + (identical(other.stackTrace, stackTrace) || + other.stackTrace == stackTrace)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, message, stackTrace); + + @override + String toString() { + return 'CustomLintResponse.error(id: $id, message: $message, stackTrace: $stackTrace)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintResponseErrorCopyWith<$Res> + implements $CustomLintResponseCopyWith<$Res> { + factory $CustomLintResponseErrorCopyWith(CustomLintResponseError value, + $Res Function(CustomLintResponseError) _then) = + _$CustomLintResponseErrorCopyWithImpl; + @override + @useResult + $Res call({String id, String message, String stackTrace}); +} + +/// @nodoc +class _$CustomLintResponseErrorCopyWithImpl<$Res> + implements $CustomLintResponseErrorCopyWith<$Res> { + _$CustomLintResponseErrorCopyWithImpl(this._self, this._then); + + final CustomLintResponseError _self; + final $Res Function(CustomLintResponseError) _then; + + /// Create a copy of CustomLintResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? id = null, + Object? message = null, + Object? stackTrace = null, + }) { + return _then(CustomLintResponseError( + id: null == id + ? _self.id + : id // ignore: cast_nullable_to_non_nullable + as String, + message: null == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String, + stackTrace: null == stackTrace + ? _self.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +CustomLintMessage _$CustomLintMessageFromJson(Map json) { + switch (json['runtimeType']) { + case 'event': + return CustomLintMessageEvent.fromJson(json); + case 'response': + return CustomLintMessageResponse.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'CustomLintMessage', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$CustomLintMessage { + /// Serializes this CustomLintMessage to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is CustomLintMessage); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'CustomLintMessage()'; + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintMessageEvent implements CustomLintMessage { + CustomLintMessageEvent(this.event, {final String? $type}) + : $type = $type ?? 'event'; + factory CustomLintMessageEvent.fromJson(Map json) => + _$CustomLintMessageEventFromJson(json); + + final CustomLintEvent event; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + Map toJson() { + return _$CustomLintMessageEventToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintMessageEvent && + (identical(other.event, event) || other.event == event)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, event); + + @override + String toString() { + return 'CustomLintMessage.event(event: $event)'; + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintMessageResponse implements CustomLintMessage { + CustomLintMessageResponse(this.response, {final String? $type}) + : $type = $type ?? 'response'; + factory CustomLintMessageResponse.fromJson(Map json) => + _$CustomLintMessageResponseFromJson(json); + + final CustomLintResponse response; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + Map toJson() { + return _$CustomLintMessageResponseToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintMessageResponse && + (identical(other.response, response) || + other.response == response)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, response); + + @override + String toString() { + return 'CustomLintMessage.response(response: $response)'; + } +} + +CustomLintEvent _$CustomLintEventFromJson(Map json) { + switch (json['runtimeType']) { + case 'analyzerPluginNotification': + return CustomLintEventAnalyzerPluginNotification.fromJson(json); + case 'error': + return CustomLintEventError.fromJson(json); + case 'print': + return CustomLintEventPrint.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'CustomLintEvent', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$CustomLintEvent { + /// Serializes this CustomLintEvent to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is CustomLintEvent); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'CustomLintEvent()'; + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintEventAnalyzerPluginNotification implements CustomLintEvent { + CustomLintEventAnalyzerPluginNotification( + @NotificationJsonConverter() this.notification, + {final String? $type}) + : $type = $type ?? 'analyzerPluginNotification'; + factory CustomLintEventAnalyzerPluginNotification.fromJson( + Map json) => + _$CustomLintEventAnalyzerPluginNotificationFromJson(json); + + @NotificationJsonConverter() + final Notification notification; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintEventAnalyzerPluginNotificationCopyWith< + CustomLintEventAnalyzerPluginNotification> + get copyWith => _$CustomLintEventAnalyzerPluginNotificationCopyWithImpl< + CustomLintEventAnalyzerPluginNotification>(this, _$identity); + + @override + Map toJson() { + return _$CustomLintEventAnalyzerPluginNotificationToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintEventAnalyzerPluginNotification && + (identical(other.notification, notification) || + other.notification == notification)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, notification); + + @override + String toString() { + return 'CustomLintEvent.analyzerPluginNotification(notification: $notification)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintEventAnalyzerPluginNotificationCopyWith<$Res> { + factory $CustomLintEventAnalyzerPluginNotificationCopyWith( + CustomLintEventAnalyzerPluginNotification value, + $Res Function(CustomLintEventAnalyzerPluginNotification) _then) = + _$CustomLintEventAnalyzerPluginNotificationCopyWithImpl; + @useResult + $Res call({@NotificationJsonConverter() Notification notification}); +} + +/// @nodoc +class _$CustomLintEventAnalyzerPluginNotificationCopyWithImpl<$Res> + implements $CustomLintEventAnalyzerPluginNotificationCopyWith<$Res> { + _$CustomLintEventAnalyzerPluginNotificationCopyWithImpl( + this._self, this._then); + + final CustomLintEventAnalyzerPluginNotification _self; + final $Res Function(CustomLintEventAnalyzerPluginNotification) _then; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? notification = null, + }) { + return _then(CustomLintEventAnalyzerPluginNotification( + null == notification + ? _self.notification + : notification // ignore: cast_nullable_to_non_nullable + as Notification, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintEventError implements CustomLintEvent { + CustomLintEventError(this.message, this.stackTrace, + {required this.pluginName, final String? $type}) + : $type = $type ?? 'error'; + factory CustomLintEventError.fromJson(Map json) => + _$CustomLintEventErrorFromJson(json); + + final String message; + final String stackTrace; + final String? pluginName; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintEventErrorCopyWith get copyWith => + _$CustomLintEventErrorCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$CustomLintEventErrorToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintEventError && + (identical(other.message, message) || other.message == message) && + (identical(other.stackTrace, stackTrace) || + other.stackTrace == stackTrace) && + (identical(other.pluginName, pluginName) || + other.pluginName == pluginName)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, message, stackTrace, pluginName); + + @override + String toString() { + return 'CustomLintEvent.error(message: $message, stackTrace: $stackTrace, pluginName: $pluginName)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintEventErrorCopyWith<$Res> { + factory $CustomLintEventErrorCopyWith(CustomLintEventError value, + $Res Function(CustomLintEventError) _then) = + _$CustomLintEventErrorCopyWithImpl; + @useResult + $Res call({String message, String stackTrace, String? pluginName}); +} + +/// @nodoc +class _$CustomLintEventErrorCopyWithImpl<$Res> + implements $CustomLintEventErrorCopyWith<$Res> { + _$CustomLintEventErrorCopyWithImpl(this._self, this._then); + + final CustomLintEventError _self; + final $Res Function(CustomLintEventError) _then; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? message = null, + Object? stackTrace = null, + Object? pluginName = freezed, + }) { + return _then(CustomLintEventError( + null == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String, + null == stackTrace + ? _self.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as String, + pluginName: freezed == pluginName + ? _self.pluginName + : pluginName // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class CustomLintEventPrint implements CustomLintEvent { + CustomLintEventPrint(this.message, + {required this.pluginName, final String? $type}) + : $type = $type ?? 'print'; + factory CustomLintEventPrint.fromJson(Map json) => + _$CustomLintEventPrintFromJson(json); + + final String message; + final String? pluginName; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CustomLintEventPrintCopyWith get copyWith => + _$CustomLintEventPrintCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$CustomLintEventPrintToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CustomLintEventPrint && + (identical(other.message, message) || other.message == message) && + (identical(other.pluginName, pluginName) || + other.pluginName == pluginName)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, message, pluginName); + + @override + String toString() { + return 'CustomLintEvent.print(message: $message, pluginName: $pluginName)'; + } +} + +/// @nodoc +abstract mixin class $CustomLintEventPrintCopyWith<$Res> { + factory $CustomLintEventPrintCopyWith(CustomLintEventPrint value, + $Res Function(CustomLintEventPrint) _then) = + _$CustomLintEventPrintCopyWithImpl; + @useResult + $Res call({String message, String? pluginName}); +} + +/// @nodoc +class _$CustomLintEventPrintCopyWithImpl<$Res> + implements $CustomLintEventPrintCopyWith<$Res> { + _$CustomLintEventPrintCopyWithImpl(this._self, this._then); + + final CustomLintEventPrint _self; + final $Res Function(CustomLintEventPrint) _then; + + /// Create a copy of CustomLintEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? message = null, + Object? pluginName = freezed, + }) { + return _then(CustomLintEventPrint( + null == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String, + pluginName: freezed == pluginName + ? _self.pluginName + : pluginName // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +// dart format on diff --git a/packages/custom_lint/lib/src/v2/protocol.g.dart b/packages/custom_lint/lib/src/v2/protocol.g.dart new file mode 100644 index 00000000..c5501e72 --- /dev/null +++ b/packages/custom_lint/lib/src/v2/protocol.g.dart @@ -0,0 +1,198 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: type=lint + +part of 'protocol.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CustomLintRequestAnalyzerPluginRequest + _$CustomLintRequestAnalyzerPluginRequestFromJson( + Map json) => + CustomLintRequestAnalyzerPluginRequest( + Request.fromJson(json['request'] as Map), + id: json['id'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintRequestAnalyzerPluginRequestToJson( + CustomLintRequestAnalyzerPluginRequest instance) => + { + 'request': instance.request, + 'id': instance.id, + 'runtimeType': instance.$type, + }; + +CustomLintRequestAwaitAnalysisDone _$CustomLintRequestAwaitAnalysisDoneFromJson( + Map json) => + CustomLintRequestAwaitAnalysisDone( + id: json['id'] as String, + reload: json['reload'] as bool, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintRequestAwaitAnalysisDoneToJson( + CustomLintRequestAwaitAnalysisDone instance) => + { + 'id': instance.id, + 'reload': instance.reload, + 'runtimeType': instance.$type, + }; + +CustomLintRequestPing _$CustomLintRequestPingFromJson( + Map json) => + CustomLintRequestPing( + id: json['id'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintRequestPingToJson( + CustomLintRequestPing instance) => + { + 'id': instance.id, + 'runtimeType': instance.$type, + }; + +CustomLintResponseAnalyzerPluginResponse + _$CustomLintResponseAnalyzerPluginResponseFromJson( + Map json) => + CustomLintResponseAnalyzerPluginResponse( + Response.fromJson(json['response'] as Map), + id: json['id'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintResponseAnalyzerPluginResponseToJson( + CustomLintResponseAnalyzerPluginResponse instance) => + { + 'response': instance.response, + 'id': instance.id, + 'runtimeType': instance.$type, + }; + +CustomLintResponseAwaitAnalysisDone + _$CustomLintResponseAwaitAnalysisDoneFromJson(Map json) => + CustomLintResponseAwaitAnalysisDone( + id: json['id'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintResponseAwaitAnalysisDoneToJson( + CustomLintResponseAwaitAnalysisDone instance) => + { + 'id': instance.id, + 'runtimeType': instance.$type, + }; + +CustomLintResponsePong _$CustomLintResponsePongFromJson( + Map json) => + CustomLintResponsePong( + id: json['id'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintResponsePongToJson( + CustomLintResponsePong instance) => + { + 'id': instance.id, + 'runtimeType': instance.$type, + }; + +CustomLintResponseError _$CustomLintResponseErrorFromJson( + Map json) => + CustomLintResponseError( + id: json['id'] as String, + message: json['message'] as String, + stackTrace: json['stackTrace'] as String, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintResponseErrorToJson( + CustomLintResponseError instance) => + { + 'id': instance.id, + 'message': instance.message, + 'stackTrace': instance.stackTrace, + 'runtimeType': instance.$type, + }; + +CustomLintMessageEvent _$CustomLintMessageEventFromJson( + Map json) => + CustomLintMessageEvent( + CustomLintEvent.fromJson(json['event'] as Map), + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintMessageEventToJson( + CustomLintMessageEvent instance) => + { + 'event': instance.event, + 'runtimeType': instance.$type, + }; + +CustomLintMessageResponse _$CustomLintMessageResponseFromJson( + Map json) => + CustomLintMessageResponse( + CustomLintResponse.fromJson(json['response'] as Map), + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintMessageResponseToJson( + CustomLintMessageResponse instance) => + { + 'response': instance.response, + 'runtimeType': instance.$type, + }; + +CustomLintEventAnalyzerPluginNotification + _$CustomLintEventAnalyzerPluginNotificationFromJson( + Map json) => + CustomLintEventAnalyzerPluginNotification( + const NotificationJsonConverter() + .fromJson(json['notification'] as Map), + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintEventAnalyzerPluginNotificationToJson( + CustomLintEventAnalyzerPluginNotification instance) => + { + 'notification': + const NotificationJsonConverter().toJson(instance.notification), + 'runtimeType': instance.$type, + }; + +CustomLintEventError _$CustomLintEventErrorFromJson( + Map json) => + CustomLintEventError( + json['message'] as String, + json['stackTrace'] as String, + pluginName: json['pluginName'] as String?, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintEventErrorToJson( + CustomLintEventError instance) => + { + 'message': instance.message, + 'stackTrace': instance.stackTrace, + 'pluginName': instance.pluginName, + 'runtimeType': instance.$type, + }; + +CustomLintEventPrint _$CustomLintEventPrintFromJson( + Map json) => + CustomLintEventPrint( + json['message'] as String, + pluginName: json['pluginName'] as String?, + $type: json['runtimeType'] as String?, + ); + +Map _$CustomLintEventPrintToJson( + CustomLintEventPrint instance) => + { + 'message': instance.message, + 'pluginName': instance.pluginName, + 'runtimeType': instance.$type, + }; diff --git a/packages/custom_lint/lib/src/v2/server_to_client_channel.dart b/packages/custom_lint/lib/src/v2/server_to_client_channel.dart new file mode 100644 index 00000000..769655c9 --- /dev/null +++ b/packages/custom_lint/lib/src/v2/server_to_client_channel.dart @@ -0,0 +1,330 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:path/path.dart'; +import 'package:uuid/uuid.dart'; + +import '../async_operation.dart'; +import '../channels.dart'; +import '../workspace.dart'; +import 'custom_lint_analyzer_plugin.dart'; +import 'protocol.dart'; + +Future _asyncRetry( + Future Function() cb, { + required int retryCount, +}) async { + var i = 0; + while (true) { + i++; + try { + return await cb(); + } catch (error, stackTrace) { + // Logging the error + Zone.current.handleUncaughtError(error, stackTrace); + // If out of retry, stop + if (i >= retryCount) rethrow; + } + } +} + +/// An interface for communicating with the plugins over a [Socket]. +class SocketCustomLintServerToClientChannel { + SocketCustomLintServerToClientChannel._( + this._server, + this._serverSocket, + this._socket, + this._version, + this._contextRoots, + this._workspace, + ) : _channel = JsonSocketChannel(_socket); + + /// Starts a server socket and exposes a way to communicate with potential clients. + /// + /// Returns `null` if the workspace has no plugins enabled. + static Future create( + CustomLintServer server, + PluginVersionCheckParams version, + AnalysisSetContextRootsParams contextRoots, { + required Directory workingDirectory, + }) async { + final workspace = await CustomLintWorkspace.fromContextRoots( + contextRoots.roots, + workingDirectory: workingDirectory, + ); + if (workspace.uniquePluginNames.isEmpty) return null; + + final serverSocket = await _createServerSocket(); + + return SocketCustomLintServerToClientChannel._( + server, + serverSocket, + // Voluntarily thow if no client connected + serverSocket.safeFirst, + version, + contextRoots, + workspace, + ); + } + + Directory? _tempDirectory; + + final Future _socket; + final JsonSocketChannel _channel; + final CustomLintServer _server; + final PluginVersionCheckParams _version; + final ServerSocket _serverSocket; + late final Future _processFuture; + final CustomLintWorkspace _workspace; + + AnalysisSetContextRootsParams _contextRoots; + + late final Stream _messages = _channel.messages + .map((e) => e! as Map) + .map(CustomLintMessage.fromJson); + + late final Stream _responses = _messages + .where((msg) => msg is CustomLintMessageResponse) + .cast() + .map((e) => e.response); + + /// The events sent by the client. + late final Stream events = _messages + .where((msg) => msg is CustomLintMessageEvent) + .cast() + .map((eventMsg) => eventMsg.event); + + static Future _createServerSocket() async { + try { + return await ServerSocket.bind(InternetAddress.loopbackIPv6, 0); + } on SocketException { + return ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + } + } + + /// Initializes and waits for the client to start + Future init({required bool debug}) async { + _processFuture = _startProcess(debug: debug); + final process = await _processFuture; + // No process started, likely because no plugin were detected. We can stop here. + if (process == null) return; + + final out = process.stdout.map(utf8.decode); + + out.listen((event) => _server.handlePrint(event, isClientMessage: true)); + process.stderr + .map(utf8.decode) + .listen((e) => _server.handleUncaughtError(e, StackTrace.empty)); + + // Checking process failure _after_ piping stdout/stderr to the log files. + // This is so that if client failed to boot, logs in it should still be available + await _checkInitializationFail(process); + + await Future.wait([ + sendAnalyzerPluginRequest(_version.toRequest(const Uuid().v4())), + sendAnalyzerPluginRequest(_contextRoots.toRequest(const Uuid().v4())), + ]); + } + + /// Updates the context roots on the client + Future setContextRoots( + AnalysisSetContextRootsParams contextRoots, + ) async { + _contextRoots = contextRoots; + return AnalysisSetContextRootsResult(); + } + + /// Encapsulates all the logic for initializing the process, + /// without setting up the connection. + /// + /// Will throw if the process fails to start. + Future _startProcess({ + required bool debug, + }) async { + final tempDir = _tempDirectory = + Directory.systemTemp.createTempSync('custom_lint_client'); + + try { + await _workspace.resolvePluginHost(tempDir); + _writeEntrypoint(_workspace.uniquePluginNames, tempDir); + + return _asyncRetry(retryCount: 5, () async { + final process = await Process.start( + Platform.resolvedExecutable, + [ + if (_server.watchMode ?? debug) '--enable-vm-service=0', + join('lib', 'custom_lint_client.dart'), + _serverSocket.address.host, + _serverSocket.port.toString(), + ], + workingDirectory: tempDir.path, + ); + return process; + }); + } catch (_) { + // If the process failed to start, we can delete the temp directory + await _tempDirectory?.delete(recursive: true); + rethrow; + } + } + + void _writeEntrypoint( + Iterable pluginNames, + Directory tempDirectory, + ) { + final imports = pluginNames + .map((name) => "import 'package:$name/$name.dart' as $name;\n") + .join(); + + final plugins = pluginNames + .map((pluginName) => "'$pluginName': $pluginName.createPlugin,\n") + .join(); + + final mainFile = File( + join(tempDirectory.path, 'lib', 'custom_lint_client.dart'), + ); + mainFile.createSync(recursive: true); + mainFile.writeAsStringSync(''' +import 'dart:convert'; +import 'dart:io'; +import 'package:custom_lint_builder/src/channel.dart'; +$imports + +void main(List args) async { + final host = args[0]; + final port = int.parse(args[1]); + + runSocket( + port: port, + host: host, + fix: ${_server.fix}, + includeBuiltInLints: ${_server.includeBuiltInLints}, + {$plugins}, + ); +} +'''); + } + + Future _checkInitializationFail(Process process) async { + var running = true; + try { + return await Future.any([ + _socket, + process.exitCode.then((exitCode) async { + // A socket was returned before the exitCode was obtained. + // As such, the process correctly started + if (!running) return; + + await _server.handlePluginInitializationFail(); + + throw StateError('Failed to start the plugins.'); + }), + ]); + } finally { + running = false; + } + } + + /// Sends a request based on the analyzer_plugin protocol, expecting + /// an analyzer_plugin response. + Future sendAnalyzerPluginRequest(Request request) async { + final response = await sendCustomLintRequest( + CustomLintRequest.analyzerPluginRequest(request, id: request.id), + ); + + return switch (response) { + CustomLintResponseAnalyzerPluginResponse() => response.response, + CustomLintResponseAwaitAnalysisDone() || + CustomLintResponsePong() || + CustomLintResponseError() => + throw UnsupportedError( + 'Expected a CustomLintResponse.analyzerPluginResponse ' + 'but received ${response.runtimeType}.', + ) + }; + } + + /// Sends a custom_lint request to the client, expecting a custom_lint response + Future sendCustomLintRequest( + CustomLintRequest request, + ) async { + final matchingResponse = _responses.firstWhere( + (e) => e.id == request.id, + orElse: () => throw StateError( + 'No response for request $request', + ), + ); + + await _channel.sendJson(request.toJson()); + + final response = await matchingResponse; + + switch (response) { + case CustomLintResponseAwaitAnalysisDone(): + case CustomLintResponsePong(): + break; + + case CustomLintResponseAnalyzerPluginResponse(): + final error = response.response.error; + if (error != null) { + throw CustomLintRequestFailure( + message: error.message, + stackTrace: error.stackTrace, + request: request, + ); + } + case CustomLintResponseError(): + throw CustomLintRequestFailure( + message: response.message, + stackTrace: response.stackTrace, + request: request, + ); + } + + return response; + } + + /// Stops the client, liberating the resources. + Future close() async { + // TODO send shutdown request + + await Future.wait([ + if (_tempDirectory != null) _tempDirectory!.delete(recursive: true), + _socket.then((value) => value.close()), + _serverSocket.close(), + _channel.close(), + _processFuture.then( + (value) => value?.kill(), + // The process wasn't started. No need to do anything. + onError: (_) {}, + ), + ]); + } +} + +/// A custom_lint request failed +class CustomLintRequestFailure implements Exception { + /// A custom_lint request failed + CustomLintRequestFailure({ + required this.message, + required this.stackTrace, + required this.request, + }); + + /// The error message + final String message; + + /// The stacktrace of the error + final String? stackTrace; + + /// The request that failed. + final CustomLintRequest request; + + @override + String toString() { + return 'A request threw the exception:$message\n$stackTrace'; + } +} diff --git a/packages/custom_lint/lib/src/workspace.dart b/packages/custom_lint/lib/src/workspace.dart new file mode 100644 index 00000000..2d5bfd82 --- /dev/null +++ b/packages/custom_lint/lib/src/workspace.dart @@ -0,0 +1,1210 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_generated.dart' + as analyzer_plugin; +import 'package:async/async.dart'; +import 'package:collection/collection.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:yaml/yaml.dart'; + +/// Compute the constraint for a dependency which matches with all the constraints +/// used in the workspace. +String _buildDependencyConstraint( + String name, + List<({CustomLintProject project, Dependency dependency})> dependencies, { + required Directory workingDirectory, + required String fileName, +}) { + // We can't pick the "first" then use .skip(1) because the pattern match + // may transform the shared constraint. Such as modifying path dependencies + // to all use absolute paths. + Dependency? sharedConstraint; + for (final (:project, :dependency) in dependencies) { + final dependencyMeta = dependencies.map( + (d) => DependencyConstraintMeta.fromDependency( + d.dependency, + d.project, + workingDirectory: workingDirectory, + ), + ); + + Never throws() => throw IncompatibleDependencyConstraintsException( + ConflictKind.dependency(name), + dependencyMeta.toList(), + fileName: fileName, + ); + + switch ((dependency: dependency, constraint: sharedConstraint)) { + case ( + :final HostedDependency dependency, + :final HostedDependency? constraint, + ): + sharedConstraint = dependency; + + if (constraint == null) continue; + + if (constraint.hosted?.declaredName != + dependency.hosted?.declaredName || + constraint.hosted?.url != dependency.hosted?.url) { + throws(); + } + + final newConstraint = constraint.version.intersect(dependency.version); + if (newConstraint.isEmpty) throws(); + + sharedConstraint = HostedDependency( + version: newConstraint, + hosted: constraint.hosted, + ); + + case ( + :final PathDependency dependency, + :final PathDependency? constraint, + ): + final absoluteDependencyPath = normalize( + absolute( + project.directory.path, + dependency.path, + ), + ); + sharedConstraint = PathDependency(absoluteDependencyPath); + + if (constraint == null) continue; + if (constraint.path != absoluteDependencyPath) throws(); + + case ( + :final SdkDependency dependency, + :final SdkDependency? constraint, + ): + sharedConstraint = dependency; + + if (constraint == null) continue; + if (constraint.sdk != dependency.sdk) throws(); + + case ( + :final GitDependency dependency, + :final GitDependency? constraint, + ): + sharedConstraint = dependency; + + if (constraint == null) continue; + if (constraint.url != dependency.url || + constraint.path != dependency.path || + constraint.ref != dependency.ref) { + throws(); + } + + default: + throws(); + } + } + + switch (sharedConstraint) { + case HostedDependency(): + final hosted = sharedConstraint.hosted; + if (hosted == null) { + return ' ${sharedConstraint.getDisplayString()}'; + } + final result = StringBuffer(); + result.writeln(); + result.writeln(' hosted:'); + if (hosted.declaredName != null) { + result.writeln(' name: ${hosted.declaredName}'); + } + if (hosted.url != null) { + result.writeln(' url: ${hosted.url}'); + } + result.write(' version: ${sharedConstraint.getDisplayString()}'); + return result.toString(); + + case PathDependency(): + // Use appropriate path separators across platforms + final path = posix.prettyUri(sharedConstraint.path); + return '\n path: "$path"'; + case SdkDependency(): + return '\n sdk: ${sharedConstraint.sdk}'; + case GitDependency(): + final result = StringBuffer('\n git:'); + result.write('\n url: ${sharedConstraint.url}'); + + if (sharedConstraint.ref != null) { + result.write('\n ref: ${sharedConstraint.ref}'); + } + if (sharedConstraint.path != null) { + result.write('\n path: "${sharedConstraint.path}"'); + } + + return result.toString(); + case _: + throw StateError( + 'Unknown constraint type: ${sharedConstraint.runtimeType}', + ); + } +} + +/// A type of version conflict +sealed class ConflictKind { + /// A conflict between two dependencies from different packages in the same workspace. + factory ConflictKind.dependency(String packageName) = _DependencyConflict; + + /// A conflict between two dependencies from the same package in the same workspace. + factory ConflictKind.environment(String packageName) = _EnvironmentConflict; + + /// The human readable name of the conflict type. + String get kindDisplayString; + + /// The value of the conflict. + String get value; +} + +class _EnvironmentConflict implements ConflictKind { + _EnvironmentConflict(this.key); + + @override + String get kindDisplayString => 'environment'; + + @override + String get value => key; + + final String key; +} + +class _DependencyConflict implements ConflictKind { + _DependencyConflict(this.packageName); + + @override + String get kindDisplayString => 'package'; + + @override + String get value => packageName; + + final String packageName; +} + +/// Information related to a dependency and the project it is used in. +class DependencyConstraintMeta { + DependencyConstraintMeta._( + this.dependencyDisplayString, + CustomLintProject project, { + required Directory workingDirectory, + }) : projectName = project.pubspec.name, + projectPath = join( + '.', + normalize( + relative(project.directory.path, from: workingDirectory.path), + ), + ); + + /// Construct a [DependencyConstraintMeta] from a [VersionConstraint]. + DependencyConstraintMeta.fromVersionConstraint( + VersionConstraint constraint, + CustomLintProject project, { + required Directory workingDirectory, + }) : this._( + HostedDependency(version: constraint).getDisplayString(), + project, + workingDirectory: workingDirectory, + ); + + /// Construct a [DependencyConstraintMeta] from a [Dependency]. + DependencyConstraintMeta.fromDependency( + Dependency dependency, + CustomLintProject project, { + required Directory workingDirectory, + }) : this._( + dependency.getDisplayString(), + project, + workingDirectory: workingDirectory, + ); + + /// Either a [VersionConstraint] or a [Dependency]. + final String dependencyDisplayString; + + /// The name of the project which uses the dependency. + final String projectName; + + /// The path to the project which uses the dependency. + final String projectPath; +} + +extension on Dependency { + String getDisplayString() { + final that = this; + return switch (that) { + HostedDependency() when that.version == VersionConstraint.any => 'any', + HostedDependency() => '"${that.version}"', + PathDependency() => '"${that.path}"', + SdkDependency() => 'sdk: ${that.sdk}', + GitDependency() => 'git: ${that.url}', + }; + } +} + +/// {@template IncompatibleDependencyConstraintsException} +/// An exception thrown when a dependency is used with different constraints +/// {@endtemplate} +class IncompatibleDependencyConstraintsException implements Exception { + /// {@macro IncompatibleDependencyConstraintsException} + IncompatibleDependencyConstraintsException( + this.kind, + this.conflictingDependencies, { + required this.fileName, + }) : assert( + conflictingDependencies.length > 1, + 'Must have at least 2 items', + ); + + /// The name of the file where the conflict was found. + final String fileName; + + /// The type of conflict. + final ConflictKind kind; + + /// The conflicting dependencies. + final List conflictingDependencies; + + @override + String toString() { + final buffer = StringBuffer( + 'The ${kind.kindDisplayString} "${kind.value}" has incompatible version constraints in the project:\n', + ); + + for (final DependencyConstraintMeta( + dependencyDisplayString: dependency, + :projectName, + :projectPath + ) in conflictingDependencies) { + buffer.write(''' +- $dependency + from "$projectName" at "${join(projectPath, fileName)}". +'''); + } + + return buffer.toString(); + } +} + +/// An exception thrown by [visitAnalysisOptionAndIncludes] when an "include" +/// directive creates a cycle. +class CyclicIncludeException implements Exception { + CyclicIncludeException._(this.path); + + /// The path that ends-up including itself. + final String path; + + @override + String toString() => 'Cyclic include detected: $path'; +} + +/// Returns a stream of YAML maps obtained by recursively following the "include" +/// keys in an analysis options file, starting from the given [analysisOptionsFile]. +/// +/// The function yields the YAML map in the original analysis options file first, +/// and then yields the YAML maps in the included files in order. + +/// If the analysis options file does not exist or is not a YAML map, or if +/// any included file does not exist or is not a YAML map, the function skips +/// that file and will end execution. If no YAML maps are found in the +/// analysis options file or its included files, the function returns +/// an empty stream. +/// +/// +/// If an included file contains a "package" URI scheme, the function resolves +/// the URI using the `package_config.json` file in the same directory as the +/// [analysisOptionsFile]. +/// If the `package_config.json` file does not exist or the `package_config.json` +/// does not contain the imported package, the function will stop its execution. +/// +/// If any included file is visited multiple times, the function throws a +/// [CyclicIncludeException] indicating a cycle in the include graph. +Stream visitAnalysisOptionAndIncludes( + File analysisOptionsFile, +) async* { + final visited = {}; + late final packageConfigFuture = loadPackageConfig( + File( + join(analysisOptionsFile.parent.path, '.dart_tool/package_config.json'), + ), + ).then( + (value) => value, + // On error, return null to not throw. The function later handles the null + onError: (e, s) => null, + ); + + for (Uri? optionsPath = analysisOptionsFile.uri; optionsPath != null;) { + final optionsFile = File.fromUri(optionsPath); + if (!visited.add(optionsFile.path)) { + // The file was visited multiple times. This is a cycle. + throw CyclicIncludeException._(optionsFile.path); + } + + if (!optionsFile.existsSync()) return; + + final yaml = loadYaml(optionsFile.readAsStringSync()); + if (yaml is! YamlMap) return; + + yield yaml; + + final includePath = yaml['include']; + if (includePath is! String) return; + + final includeUri = Uri.tryParse(includePath); + if (includeUri == null) return; + + if (includeUri.scheme == 'package') { + final packageName = includeUri.pathSegments.first; + final packageConfig = await packageConfigFuture; + + // Search for the package with matching name in packageConfig + final package = packageConfig?.packages.firstWhereOrNull( + (package) => package.name == packageName, + ); + if (package == null) return; + + final packageRoot = Directory.fromUri(package.packageUriRoot); + final packagePath = join( + packageRoot.path, + // Skip the first segment, which is the package name. + // In package:foo/src/file.dart, we only care about src/file.dart + joinAll(includeUri.pathSegments.skip(1)), + ); + optionsPath = Uri.file(packagePath); + continue; + } + + optionsPath = optionsPath.resolveUri(includeUri); + } +} + +/// A typedef for [Process.run]. +typedef RunProcess = Future Function( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment, + bool runInShell, + Encoding? stdoutEncoding, + Encoding? stderrEncoding, +}); + +/// A mockable way to run processes. +@visibleForTesting +RunProcess runProcess = Process.run; + +/// Allow mocking of the platform for tests. +@visibleForTesting +bool platformIsWindows = Platform.isWindows; + +String? _findOptionsForPubspec(String pubspecPath) { + final analysisOptions = File(join(pubspecPath, 'analysis_options.yaml')); + if (analysisOptions.existsSync()) return analysisOptions.path; + + final parent = Directory(pubspecPath).parent; + if (parent.path == pubspecPath) return null; + + return _findOptionsForPubspec(parent.path); +} + +Iterable _findRoots(String path) sync* { + final directory = Directory(path); + + yield* directory.listSync(recursive: true).whereType().where((file) { + final fileName = basename(file.path); + if (fileName != 'pubspec.yaml' && fileName != 'analysis_options.yaml') { + return false; + } + + return file.parent.packageConfig.existsSync(); + }).map((file) => file.parent.path); +} + +/// The holder of metadatas related to the enabled plugins and analyzed projects. +@internal +class CustomLintWorkspace { + /// Creates a new workspace. + CustomLintWorkspace._( + this.projects, + this.contextRoots, + this.uniquePluginNames, { + required this.workingDirectory, + }); + + /// Initializes the custom_lint workspace from a directory. + static Future fromPaths( + List paths, { + required Directory workingDirectory, + }) async { + final distinctRoots = paths + .map((e) => normalize(absolute(e, workingDirectory.path))) + .expand(_findRoots) + .toSet(); + final foundRoots = await Future.wait( + distinctRoots.map((rootPath) async { + final projectDir = tryFindProjectDirectory(Directory(rootPath)); + if (projectDir == null) return null; + + final pubspec = await tryParsePubspec(projectDir); + if (pubspec == null) return null; + + final optionFile = _findOptionsForPubspec(rootPath); + if (optionFile == null) return null; + + final options = File(optionFile); + + final pluginDefinition = await _isCustomLintEnabled(options); + if (!pluginDefinition) { + return null; + } + + return ( + rootPath, + optionsFile: optionFile, + ); + }), + ); + + return fromContextRoots( + foundRoots.nonNulls + .map( + (e) => analyzer_plugin.ContextRoot( + e.$1, + [ + for (final otherPath in foundRoots) + if (otherPath != null && isWithin(e.$1, otherPath.$1)) + otherPath.$1, + ], + optionsFile: e.optionsFile, + ), + ) + .toList(), + workingDirectory: workingDirectory, + ); + } + + static Future _isCustomLintEnabled(File options) async { + final enabledPlugins = await visitAnalysisOptionAndIncludes(options) + .map((event) { + final analyzerMap = event['analyzer']; + if (analyzerMap is! YamlMap) return null; + return analyzerMap['plugins']; + }) + .whereNotNull() + .firstOrNull; + + if (enabledPlugins is! YamlList) return false; + + return enabledPlugins.contains('custom_lint'); + } + + /// Initializes the custom_lint workspace from a compilation of context roots. + static Future fromContextRoots( + List contextRoots, { + required Directory workingDirectory, + }) async { + final cache = CustomLintPluginCheckerCache(); + final projects = await Future.wait([ + for (final contextRoot in contextRoots) + CustomLintProject.parse(contextRoot, cache), + ]); + + final uniquePluginNames = + projects.expand((e) => e.plugins).map((e) => e.name).toSet(); + + final realProjects = projects.where((e) => e.isProjectRoot).toList(); + + return CustomLintWorkspace._( + realProjects, + contextRoots, + uniquePluginNames, + workingDirectory: workingDirectory, + ); + } + + /// Whether the workspace is using flutter. + bool get isUsingFlutter => projects + .expand((e) => e.packageConfig.packages) + .any((e) => e.name == 'flutter'); + + /// The working directory of the workspace. + /// This is the directory from which the workspace was initialized. + final Directory workingDirectory; + + /// The list of analyzed projects. + final List contextRoots; + + /// The list of analyzed projects. + final List projects; + + /// The names of all enabled plugins. + final Set uniquePluginNames; + + /// A method to generate a `pubspec.yaml` in the client project + /// + /// This is the combination of all `pubspec.yaml` in the workspace. + @internal + String computePubspec() { + final buffer = StringBuffer(''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' +'''); + + _writeEnvironment(buffer); + _writePubspecDependencies(buffer); + + return buffer.toString(); + } + + void _writeEnvironment(StringBuffer buffer) { + final environmentKeys = + projects.expand((e) => e.pubspec.environment.keys).toSet(); + + if (environmentKeys.isEmpty) return; + + buffer.writeln('\nenvironment:'); + + for (final key in environmentKeys) { + final projectMeta = projects + .map((project) { + final constraint = project.pubspec.environment[key]; + if (constraint == null) return null; + return (project: project, constraint: constraint); + }) + // TODO what if some projects specify SDK/Flutter but some don't? + .nonNulls + .toList(); + + final constraintCompatibleWithAllProjects = projectMeta.fold( + VersionConstraint.parse('^3.0.0'), + (acc, constraint) => acc.intersect(constraint.constraint), + ); + + if (constraintCompatibleWithAllProjects.isEmpty) { + throw IncompatibleDependencyConstraintsException( + ConflictKind.environment(key), + projectMeta + .map( + (e) => DependencyConstraintMeta.fromVersionConstraint( + e.constraint, + e.project, + workingDirectory: workingDirectory, + ), + ) + .toList(), + fileName: 'pubspec.yaml', + ); + } + + buffer.writeln(' $key: "$constraintCompatibleWithAllProjects"'); + } + } + + void _writePubspecDependencies(StringBuffer buffer) { + // Collect all the dependencies for each package. + final uniqueDependencyNames = projects.expand((e) sync* { + yield* e.pubspec.dependencies.keys; + yield* e.pubspec.devDependencies.keys; + yield* e.pubspec.dependencyOverrides.keys; + }).toSet(); + + final dependenciesByName = { + for (final name in uniqueDependencyNames) + name: ( + dependencies: projects.expand((project) { + final dependency = project.pubspec.dependencies[name]; + final devDependency = project.pubspec.devDependencies[name]; + return [ + if (dependency != null) + (project: project, dependency: dependency), + if (devDependency != null) + (project: project, dependency: devDependency), + ]; + }).toList(), + dependencyOverrides: projects + .map((project) { + final dependency = project.pubspec.dependencyOverrides[name]; + if (dependency == null) return null; + return (project: project, dependency: dependency); + }) + .nonNulls + .toList(), + ), + }; + + final dependencies = {}; + + // Iterate over each plugin and compute their constraints. + for (final name in uniquePluginNames) { + final allDependencies = dependenciesByName[name]; + if (allDependencies == null) continue; + + if (allDependencies.dependencies.isEmpty) { + continue; + } + + final constraint = allDependencies.dependencyOverrides.isNotEmpty + ? ' any' + : _buildDependencyConstraint( + name, + allDependencies.dependencies, + workingDirectory: workingDirectory, + fileName: 'pubspec.yaml', + ); + dependencies[name] = constraint; + } + + // Write the dependencies to the buffer. + if (dependencies.isNotEmpty) { + buffer.writeln('\ndependencies:'); + for (final dependency in dependencies.entries) { + buffer.writeln(' ${dependency.key}:${dependency.value}'); + } + } + + // Write the dependency_overrides to the buffer. + _writeDependencyOverrides( + buffer, + dependencyOverrides: { + for (final entry in dependenciesByName.entries) + if (entry.value.dependencyOverrides.isNotEmpty) + entry.key: entry.value.dependencyOverrides, + }, + ); + } + + void _writeDependencyOverrides( + StringBuffer buffer, { + required Map> + dependencyOverrides, + }) { + var didWriteDependencyOverridesHeader = false; + for (final entry in dependencyOverrides.entries) { + if (!didWriteDependencyOverridesHeader) { + didWriteDependencyOverridesHeader = true; + // Add empty line to separate dependency_overrides from other dependencies. + if (buffer.isNotEmpty) buffer.writeln(); + buffer.writeln('dependency_overrides:'); + } + + final constraint = _buildDependencyConstraint( + entry.key, + entry.value, + workingDirectory: workingDirectory, + fileName: 'pubspec_overrides.yaml', + ); + buffer.writeln(' ${entry.key}:$constraint'); + } + } + + /// A method to generate a `pubspec_overrides.yaml` in the client project. + /// + /// This is the combination of all `pubspec_overrides.yaml` in the workspace. + @internal + String? computePubspecOverride() { + final uniqueDependencyNames = projects // + .expand((e) => e.pubspecOverrides?.keys ?? []) + .toSet(); + + if (uniqueDependencyNames.isEmpty) return null; + + final dependenciesByName = { + for (final name in uniqueDependencyNames) + name: projects + .map((project) { + final dependency = project.pubspecOverrides?[name]; + if (dependency == null) return null; + return (project: project, dependency: dependency); + }) + .nonNulls + .toList(), + }; + + final buffer = StringBuffer(); + + _writeDependencyOverrides( + buffer, + dependencyOverrides: dependenciesByName, + ); + + return buffer.toString(); + } + + /// First attempts at creating the plugin host locally. And if it fails, + /// it will fallback to resolving packages using "pub get". + Future resolvePluginHost( + Directory tempDir, + ) async { + final pubspecContent = computePubspec(); + final pubspecOverride = computePubspecOverride(); + + tempDir.pubspec.writeAsStringSync(pubspecContent); + if (pubspecOverride != null) { + tempDir.pubspecOverrides.writeAsStringSync(pubspecOverride); + } + + await runPubGet(tempDir); + } + + /// Run "pub get" in the client project. + Future runPubGet(Directory tempDir) async { + final command = Platform.resolvedExecutable; + final result = await runProcess( + command, + const ['pub', 'get'], + stdoutEncoding: utf8, + stderrEncoding: utf8, + workingDirectory: tempDir.path, + runInShell: platformIsWindows, + ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run "pub get" in the client project:\n' + '${result.stdout}\n' + '${result.stderr}', + ); + } + } +} + +/// An util for detecting if a project is a custom_lint plugin. +@internal +class CustomLintPluginCheckerCache { + final _cache = >{}; + + /// Returns `true` if the project at [directory] is a custom_lint plugin. + /// + /// A project is considered a custom_lint plugin if it has a dependency on + /// `custom_lint_builder`. + Future isPlugin(Directory directory) { + final cached = _cache[directory]; + if (cached != null) return cached; + + return _cache[directory] = Future(() async { + final pubspec = await tryParsePubspec(directory); + if (pubspec == null) return false; + + // TODO test that dependency_overrides & dev_dependencies aren't checked. + return pubspec.dependencies.containsKey('custom_lint_builder'); + }); + } +} + +/// An util for parsing a pubspec once. +@internal +class PubspecCache { + final _cache = {}; + + /// Parses a pubspec and throws if the parsing fails. + /// + /// If the value is already cached, it will return the cached value or rethrow + /// the previously thrown error. + Pubspec call(Directory directory) { + final cached = _cache[directory]; + if (cached != null) return cached(); + + try { + final pubspec = parsePubspecSync(directory); + _cache[directory] = () => pubspec; + return pubspec; + } catch (e) { + // Indirect rethrow of an error. We can't use rethrow here. + // ignore: only_throw_errors, use_rethrow_when_possible + _cache[directory] = () => throw e; + + rethrow; + } + } +} + +/// No pubspec.yaml file was found for a plugin. +@internal +class PubspecParseError extends Error { + PubspecParseError._( + this.path, { + required this.error, + required this.errorStackTrace, + }); + + /// The path where the pubspec.yaml file was expected. + final String path; + + /// The inner error that was thrown when trying to parse the pubspec. + final Object error; + + /// The stacktrace of [error]. + final StackTrace errorStackTrace; + + @override + String toString() { + return 'Failed to read pubspec.yaml at $path:\n' + '$error\n' + '$errorStackTrace'; + } +} + +/// No .dart_tool/package_config.json file was found for a plugin. +@internal +class PackageConfigParseError extends Error { + PackageConfigParseError._( + this.path, { + required this.error, + required this.errorStackTrace, + }); + + /// The path where the pubspec.yaml file was expected. + final String path; + + /// The inner error that was thrown when trying to parse the pubspec. + final Object error; + + /// The stacktrace of [error]. + final StackTrace errorStackTrace; + + @override + String toString() => + 'Failed to decode .dart_tool/package_config.json at $path. ' + 'Make sure to run `pub get` first.\n' + '$error\n' + '$errorStackTrace'; +} + +/// The plugin was not found in the package config. +@internal +class PluginNotFoundInPackageConfigError extends Error { + PluginNotFoundInPackageConfigError._(this.name, this.path); + + /// The name of the plugin. + final String name; + + /// The path where the pubspec.yaml file was expected. + final String path; + + @override + String toString() => 'The plugin $name was not found in the package config ' + 'at $path. Make sure to run `pub get` first.'; +} + +/// A project analyzed by custom_lint, with its enabled plugins. +@internal +class CustomLintProject { + CustomLintProject._({ + required this.plugins, + required this.directory, + required this.packageConfig, + required this.pubspec, + required this.pubspecOverrides, + required this.analysisDirectory, + }); + + /// Decode a [CustomLintProject] from a directory. + static Future parse( + analyzer_plugin.ContextRoot contextRoot, + CustomLintPluginCheckerCache cache, + ) async { + final directory = Directory(contextRoot.root); + final projectDirectory = findProjectDirectory(directory); + final projectPubspec = await parsePubspec(projectDirectory).catchError( + // ignore: avoid_types_on_closure_parameters, false positive + (Object err, StackTrace stack) { + throw PubspecParseError._( + directory.path, + error: err, + errorStackTrace: stack, + ); + }); + final pubspecOverrides = await tryParsePubspecOverrides(projectDirectory); + final projectPackageConfig = await parsePackageConfig(projectDirectory) + // ignore: avoid_types_on_closure_parameters, false positive + .catchError((Object err, StackTrace stack) { + throw PackageConfigParseError._( + directory.path, + error: err, + errorStackTrace: stack, + ); + }); + + final plugins = await Future.wait( + { + ...projectPubspec.dependencies, + ...projectPubspec.devDependencies, + }.entries.map((e) async { + final packageWithName = projectPackageConfig.packages + .firstWhereOrNull((p) => p.name == e.key); + if (packageWithName == null) { + throw PluginNotFoundInPackageConfigError._(e.key, directory.path); + } + + final pluginDirectory = Directory.fromUri(packageWithName.root); + final isPlugin = await cache.isPlugin(pluginDirectory); + if (!isPlugin) return null; + + // TODO test error + final pluginPubspec = await parsePubspec(pluginDirectory); + + return CustomLintPlugin._( + name: e.key, + directory: pluginDirectory, + pubspec: pluginPubspec, + package: packageWithName, + constraint: PubspecDependency.fromDependency(e.value), + ownerPubspec: projectPubspec, + ownerPackageConfig: projectPackageConfig, + ); + }), + ); + + return CustomLintProject._( + plugins: plugins.nonNulls.toList(), + directory: projectDirectory, + analysisDirectory: directory, + packageConfig: projectPackageConfig, + pubspec: projectPubspec, + pubspecOverrides: pubspecOverrides, + ); + } + + /// The resolved package_config.json at the moment of parsing. + final PackageConfig packageConfig; + + /// The pubspec.yaml at the moment of parsing. + final Pubspec pubspec; + + /// The pubspec.yaml at the moment of parsing. + final Map? pubspecOverrides; + + /// The folder of the project being analyzed. + /// Generally, where the pubspec.yaml is located + final Directory directory; + + /// The enabled plugins for this project. + final List plugins; + + /// Where the analysis options file is located + /// It could be null if the project doesn't have an analysis options file. + /// + /// The analysis options file doesn't not have to be in [directory] + final Directory? analysisDirectory; + + bool get isProjectRoot { + return analysisDirectory == directory; + } +} + +/// A custom_lint plugin and its version constraints. +@internal +class CustomLintPlugin { + CustomLintPlugin._({ + required this.name, + required this.directory, + required this.constraint, + required this.ownerPackageConfig, + required this.ownerPubspec, + required this.package, + required this.pubspec, + }); + + /// The plugin name. + final String name; + + /// The directory containing the source of the plugin according to the + /// project's package_config.json. + /// + /// See also [ownerPackageConfig]. + final Directory directory; + + /// The resolved pubspec.yaml of the plugin. + final Pubspec pubspec; + + /// The pubspec of the project which depends on this plugin. + final Pubspec ownerPubspec; + + /// The resolved package_config.json metadata of this plugin. + /// + /// This can be found in [ownerPackageConfig]. + final Package package; + + /// The resolved package_config.json of the project which depends on this plugin. + final PackageConfig ownerPackageConfig; + + /// The version constraints in the project's `pubspec.yaml`. + final PubspecDependency constraint; +} + +/// A dependency in a `pubspec.yaml`. +/// +/// This is used for easy comparison between the constraints of the same +/// plugin used by different projects. +/// +/// See also [intersect] and [isCompatibleWith]. +@internal +abstract class PubspecDependency { + const PubspecDependency._(); + + /// A dependency using `git` + factory PubspecDependency.fromGitDependency(GitDependency dependency) = + GitPubspecDependency; + + /// A path dependency. + factory PubspecDependency.fromPathDependency(PathDependency dependency) = + PathPubspecDependency; + + /// A dependency using `hosted` (pub.dev) + factory PubspecDependency.fromHostedDependency(HostedDependency dependency) = + HostedPubspecDependency; + + /// A dependency using `sdk` + factory PubspecDependency.fromSdkDependency(SdkDependency dependency) = + SdkPubspecDependency; + + /// Automatically converts any [Dependency] into a [PubspecDependency]. + factory PubspecDependency.fromDependency(Dependency dependency) { + if (dependency is HostedDependency) { + return PubspecDependency.fromHostedDependency(dependency); + } else if (dependency is GitDependency) { + return PubspecDependency.fromGitDependency(dependency); + } else if (dependency is PathDependency) { + return PubspecDependency.fromPathDependency(dependency); + } else if (dependency is SdkDependency) { + return PubspecDependency.fromSdkDependency(dependency); + } else { + throw ArgumentError.value(dependency, 'dependency', 'Unknown dependency'); + } + } + + /// Builds a short description of this dependency. + String buildShortDescription(); + + /// Checks whether this and [dependency] can both be resolved at the same time. + /// + /// For example, "^1.0.0" is not compatible with "^2.0.0", but "^1.0.0" is + /// compatible with "^1.1.0" (and vice-versa). + bool isCompatibleWith(PubspecDependency dependency); + + /// Returns the intersection of this and [dependency], or `null` if they are + /// not compatible. + PubspecDependency? intersect(PubspecDependency dependency) { + if (!isCompatibleWith(dependency)) return null; + + // ignore: avoid_returning_this, conditionally returns non-this. + return this; + } +} + +/// A dependency using `git`. +class GitPubspecDependency extends PubspecDependency { + /// A dependency using `git`. + GitPubspecDependency(this.dependency) : super._(); + + /// The original git dependency + final GitDependency dependency; + + @override + String buildShortDescription() { + final versionBuilder = StringBuffer(); + versionBuilder.write('From git url ${dependency.url}'); + final dependencyRef = dependency.ref; + if (dependencyRef != null) { + versionBuilder.write(' ref $dependencyRef'); + } + final dependencyPath = dependency.path; + if (dependencyPath != null) { + versionBuilder.write(' path $dependencyPath'); + } + return versionBuilder.toString(); + } + + @override + bool isCompatibleWith(PubspecDependency dependency) { + return dependency is GitPubspecDependency && + this.dependency.url == dependency.dependency.url && + this.dependency.ref == dependency.dependency.ref && + this.dependency.path == dependency.dependency.path; + } +} + +/// A dependency using `path` +class PathPubspecDependency extends PubspecDependency { + /// A dependency using `path` + PathPubspecDependency(this.dependency) : super._(); + + /// The original path dependency + final PathDependency dependency; + + @override + bool isCompatibleWith(PubspecDependency dependency) { + return dependency is PathPubspecDependency && + this.dependency.path == dependency.dependency.path; + } + + @override + String buildShortDescription() => 'From path ${dependency.path}'; +} + +/// A dependency using `hosted` (pub.dev) +class HostedPubspecDependency extends PubspecDependency { + /// A dependency using `hosted` (pub.dev) + HostedPubspecDependency(this.dependency) : super._(); + + /// The original hosted dependency + final HostedDependency dependency; + + @override + String buildShortDescription() { + return 'Hosted with version constraint: ${dependency.version}'; + } + + @override + bool isCompatibleWith(PubspecDependency dependency) { + return dependency is HostedPubspecDependency && + this.dependency.hosted?.name == dependency.dependency.hosted?.name && + this.dependency.hosted?.url == dependency.dependency.hosted?.url && + this.dependency.version.allowsAny(dependency.dependency.version); + } + + @override + PubspecDependency? intersect(PubspecDependency dependency) { + if (!isCompatibleWith(dependency)) return null; + + dependency as HostedPubspecDependency; + return HostedPubspecDependency( + HostedDependency( + hosted: this.dependency.hosted, + version: this.dependency.version.intersect( + dependency.dependency.version, + ), + ), + ); + } +} + +/// A dependency using `sdk` +class SdkPubspecDependency extends PubspecDependency { + /// A dependency using `sdk` + SdkPubspecDependency(this.dependency) : super._(); + + /// The original sdk dependency + final SdkDependency dependency; + + @override + bool isCompatibleWith(PubspecDependency dependency) { + return dependency is SdkPubspecDependency && + this.dependency.sdk == dependency.dependency.sdk; + } + + @override + String buildShortDescription() { + return 'From SDK: ${dependency.sdk}'; + } +} diff --git a/packages/custom_lint/log.txt b/packages/custom_lint/log.txt deleted file mode 100644 index 354884dc..00000000 --- a/packages/custom_lint/log.txt +++ /dev/null @@ -1,88 +0,0 @@ - - -2022-03-15 19:05:14.991493 main -2022-03-15 19:05:15.002356 Start custom_plugin -2022-03-15 19:05:15.133394 Got package custom_lint_analyzer_plugin_loader -2022-03-15 19:05:15.168959 Got package custom_lint -2022-03-15 19:05:15.193449 Got package example_app -2022-03-15 19:05:15.221030 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/pubspec.yaml -2022-03-15 19:05:15.221360 found plugin for riverpod_lint: riverpod_lint -2022-03-15 19:05:15.221503 spawning plugin: riverpod_lint -2022-03-15 19:05:15.223007 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/pubspec.yaml -2022-03-15 19:05:15.223143 found plugin for target_lint: target_lint -2022-03-15 19:05:15.223257 spawning plugin: target_lint -2022-03-15 19:05:15.224845 Got package custom_lint_builder -2022-03-15 19:05:26.933516 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:27.407826 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:39.645998 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:39.646551 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider) -2022-03-15 19:05:40.182550 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider) -2022-03-15 19:05:40.184814 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:42.474121 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:42.474849 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider) -2022-03-15 19:05:42.807341 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:05:42.809260 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:48:12.906470 main -2022-03-15 19:48:12.917104 Start custom_plugin -2022-03-15 19:48:13.049411 Got package custom_lint_analyzer_plugin_loader -2022-03-15 19:48:13.088470 Got package custom_lint -2022-03-15 19:48:13.110904 Got package example_app -2022-03-15 19:48:13.137223 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/pubspec.yaml -2022-03-15 19:48:13.137791 found plugin for riverpod_lint: riverpod_lint -2022-03-15 19:48:13.137975 spawning plugin: riverpod_lint -2022-03-15 19:48:13.140319 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/pubspec.yaml -2022-03-15 19:48:13.140509 found plugin for target_lint: target_lint -2022-03-15 19:48:13.140638 spawning plugin: target_lint -2022-03-15 19:48:13.142275 Got package custom_lint_builder -2022-03-15 19:48:13.231707 main -2022-03-15 19:48:13.242441 Start custom_plugin -2022-03-15 19:48:13.370901 Got package custom_lint_analyzer_plugin_loader -2022-03-15 19:48:13.408753 Got package custom_lint -2022-03-15 19:48:13.432601 Got package example_app -2022-03-15 19:48:13.457617 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/pubspec.yaml -2022-03-15 19:48:13.458075 found plugin for riverpod_lint: riverpod_lint -2022-03-15 19:48:13.458224 spawning plugin: riverpod_lint -2022-03-15 19:48:13.460446 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/pubspec.yaml -2022-03-15 19:48:13.460613 found plugin for target_lint: target_lint -2022-03-15 19:48:13.460749 spawning plugin: target_lint -2022-03-15 19:48:13.462326 Got package custom_lint_builder -2022-03-15 19:48:23.971759 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:48:23.975708 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: () -2022-03-15 19:48:24.512237 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:48:24.527710 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: (target_controller) -2022-03-15 19:48:24.576385 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 19:48:25.028436 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 22:11:44.255903 main -2022-03-15 22:11:44.268365 Start custom_plugin -2022-03-15 22:11:44.435043 Got package custom_lint_analyzer_plugin_loader -2022-03-15 22:11:44.480285 Got package custom_lint -2022-03-15 22:11:44.512465 Got package example_app -2022-03-15 22:11:44.560944 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/pubspec.yaml -2022-03-15 22:11:44.561802 found plugin for riverpod_lint: riverpod_lint -2022-03-15 22:11:44.562089 spawning plugin: riverpod_lint -2022-03-15 22:11:44.564918 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/pubspec.yaml -2022-03-15 22:11:44.565269 found plugin for target_lint: target_lint -2022-03-15 22:11:44.565523 spawning plugin: target_lint -2022-03-15 22:11:44.568431 Got package custom_lint_builder -2022-03-15 22:11:59.692943 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 22:11:59.703795 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: () -2022-03-15 22:12:00.215764 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-15 22:12:00.230906 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: (target_controller) -2022-03-16 13:41:05.822206 main -2022-03-16 13:41:05.833453 Start custom_plugin -2022-03-16 13:41:10.325282 main -2022-03-16 13:41:10.337069 Start custom_plugin -2022-03-16 13:41:11.640877 Got package custom_lint_analyzer_plugin_loader -2022-03-16 13:41:11.679978 Got package custom_lint -2022-03-16 13:41:11.711774 Got package example_app -2022-03-16 13:41:11.736080 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/riverpod_lint/pubspec.yaml -2022-03-16 13:41:11.736401 found plugin for riverpod_lint: riverpod_lint -2022-03-16 13:41:11.736540 spawning plugin: riverpod_lint -2022-03-16 13:41:11.738145 warning at /Users/remirousselet/dev/invertase/custom_lint/packages/target_lint/pubspec.yaml -2022-03-16 13:41:11.738321 found plugin for target_lint: target_lint -2022-03-16 13:41:11.738484 spawning plugin: target_lint -2022-03-16 13:41:11.740204 Got package custom_lint_builder -2022-03-16 13:41:22.565836 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-16 13:41:22.571869 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: () -2022-03-16 13:41:23.003409 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main.dart: (riverpod_final_provider, riverpod_final_provider) -2022-03-16 13:41:23.020499 got lints for /Users/remirousselet/dev/invertase/custom_lint/example_app/lib/main copy.dart: (target_controller) \ No newline at end of file diff --git a/packages/custom_lint/pubspec.lock b/packages/custom_lint/pubspec.lock deleted file mode 100644 index 9a2c3c49..00000000 --- a/packages/custom_lint/pubspec.lock +++ /dev/null @@ -1,194 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "36.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1" - analyzer_plugin: - dependency: "direct main" - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.0" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.2" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - package_config: - dependency: "direct main" - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: "direct main" - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - pub_semver: - dependency: "direct main" - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - pubspec_parse: - dependency: "direct main" - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - recase: - dependency: "direct main" - description: - name: recase - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - uuid: - dependency: "direct main" - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" -sdks: - dart: ">=2.16.0 <3.0.0" diff --git a/packages/custom_lint/pubspec.yaml b/packages/custom_lint/pubspec.yaml index 20b3de76..51d19c99 100644 --- a/packages/custom_lint/pubspec.yaml +++ b/packages/custom_lint/pubspec.yaml @@ -1,17 +1,42 @@ name: custom_lint -version: 0.0.1 -repository: https://github.com/custom_lint/river_pod +version: 0.7.5 +description: Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules. +repository: https://github.com/invertase/dart_custom_lint +issue_tracker: https://github.com/invertase/dart_custom_lint/issues environment: - sdk: ">=2.16.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - analyzer: ^3.3.1 - analyzer_plugin: ^0.9.0 + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + args: ^2.3.1 + async: ^2.9.0 + ci: ^0.1.0 + cli_util: ^0.4.2 + collection: ^1.16.0 + custom_lint_core: 0.7.5 + freezed_annotation: ^3.0.0 + json_annotation: ^4.7.0 + meta: ^1.7.0 package_config: ^2.0.2 - path: ^1.8.1 - pubspec_parse: ^1.2.0 + path: ^1.8.0 pub_semver: ^2.1.1 - recase: ^4.0.0 - uuid: ^3.0.6 - \ No newline at end of file + pubspec_parse: ^1.5.0 + rxdart: ^0.28.0 + uuid: ">=3.0.6 <5.0.0" + yaml: ^3.1.1 + +dev_dependencies: + ansi_styles: ^0.3.2+1 + benchmark_harness: ^2.2.0 + build_runner: ^2.3.2 + file: ^7.0.0 + freezed: ^3.0.0 + glob: ^2.1.2 + json_serializable: ^6.5.4 + test: ^1.20.2 + test_process: ^2.1.0 + +executables: + custom_lint: custom_lint diff --git a/packages/custom_lint/pubspec_overrides.yaml b/packages/custom_lint/pubspec_overrides.yaml new file mode 100644 index 00000000..e5ecc242 --- /dev/null +++ b/packages/custom_lint/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: custom_lint_core,custom_lint_visitor +dependency_overrides: + custom_lint_core: + path: ../custom_lint_core diff --git a/packages/custom_lint/test/cli_process_test.dart b/packages/custom_lint/test/cli_process_test.dart new file mode 100644 index 00000000..92bddc09 --- /dev/null +++ b/packages/custom_lint/test/cli_process_test.dart @@ -0,0 +1,714 @@ +@Timeout.factor(2) +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:custom_lint/src/output/output_format.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; +import 'package:test_process/test_process.dart'; + +import 'cli_test.dart'; +import 'create_project.dart'; +import 'peer_project_meta.dart'; +import 'src/workspace_test.dart'; + +String trimDependencyOverridesWarning(Object? input) { + final string = input.toString(); + if (string + .startsWith('Warning: You are using these overridden dependencies:')) { + return string.split('\n').skip(3).join('\n'); + } + return string; +} + +void main() { + test('Exposes the Pubspec in CustomLintContext', () async { + final workspace = createTemporaryDirectory(); + + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + onRun: + r"print('${context.pubspec.name} ${context.pubspec.dependencies.keys}');", + ), + ]), + ); + + createLintUsage( + parent: workspace, + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + createLintUsage( + parent: workspace, + source: {'lib/main2.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app2', + ); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: workspace.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect( + process.stdout, + ''' +[hello_world] test_app (analyzer, analyzer_plugin) +[hello_world] test_app2 (analyzer, analyzer_plugin) +Analyzing... + + test_app/lib/main.dart:1:6 • Hello world • hello_world • INFO + test_app2/lib/main2.dart:1:6 • Hello world • hello_world • INFO + +2 issues found. +''', + ); + expect(process.exitCode, 1); + }); + + group('Correctly exits when', () { + test('running on a workspace with no plugins', () { + final app = createLintUsage(name: 'test_app'); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + + test('running on a workspace with no projects', () { + final dir = createTemporaryDirectory(); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: dir.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + + test( + 'running on a workspace with dependencies without a package_config.json', + () { + final app = createLintUsage(name: 'test_app'); + const testDepsName = 'test_deps'; + const testDepsPubSpec = ''' +name: $testDepsName +version: 0.0.1 +'''; + createTmpFolder( + { + 'pubspec.yaml': testDepsPubSpec, + }, + testDepsName, + parent: Directory(join(app.path, '.dart_tool')), + ); + createTmpFolder( + { + join('.symlinks', 'plugins', 'pubspec.yaml'): testDepsPubSpec, + }, + 'ios', + parent: app, + ); + createTmpFolder( + { + join( + 'flutter', + 'ephemeral', + '.plugin_symlinks', + 'plugin_name', + 'example', + 'pubspec.yaml', + ): testDepsPubSpec, + }, + 'linux', + parent: app, + ); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + + test( + 'no issues found', + () async { + final plugin = createPlugin(name: 'test_lint', main: emptyPluginSource); + + final app = createLintUsage( + name: 'test_app', + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }, + ); + + for (final format in OutputFormatEnum.values.map((e) => e.name)) { + test( + 'found lints format: $format', + () async { + final plugin = createPlugin(name: 'test_lint', main: oyPluginSource); + + final app = createLintUsage( + name: 'test_app', + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [ + customLintBinPath, + '--format', + format, + ], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + if (format == 'json') { + final dir = Directory(app.path).resolveSymbolicLinksSync(); + final json = jsonEncode({ + 'version': 1, + 'diagnostics': [ + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': {'offset': 5, 'line': 1, 'column': 6}, + 'end': {'offset': 9, 'line': 1, 'column': 10}, + }, + }, + 'problemMessage': 'Oy', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': {'offset': 5, 'line': 1, 'column': 6}, + 'end': {'offset': 7, 'line': 1, 'column': 8}, + }, + }, + 'problemMessage': 'Oy', + } + ], + }); + expect(process.stdout, '$json\n'); + } else { + expect(process.stdout, ''' +Analyzing... + + lib/another.dart:1:6 • Oy • oy • INFO + lib/main.dart:1:6 • Oy • oy • INFO + +2 issues found. +'''); + } + expect(process.exitCode, 1); + }, + ); + } + + test( + 'dependency conflict', + () async { + // Create two packages with the same name but different paths + final workspace = await createSimpleWorkspace( + ['dep', 'dep'], + local: true, + ); + + final plugin = createPlugin( + parent: workspace, + name: 'test_lint', + main: oyPluginSource, + extraDependencies: {'dep': 'any'}, + ); + + // We define two projects with different dependencies + final app = createLintUsage( + parent: workspace, + name: 'test_app', + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + extraPackageConfig: {'dep': workspace.dir('dep').uri}, + ); + + createLintUsage( + // Add the second project inside the first one, such that + // analyzing the first project analyzes both projects + parent: app, + name: 'test_app2', + source: {'lib/foo.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + extraPackageConfig: {'dep': workspace.dir('dep2').uri}, + ); + + final process = await Process.start( + workingDirectory: app.path, + 'dart', + [customLintBinPath], + ); + + expect(process.stdout, emitsDone); + expect( + await process.stderr + .map(utf8.decode) + .map(trimDependencyOverridesWarning) + .join('\n'), + startsWith( + ''' +The request analysis.setContextRoots failed with the following error: +RequestErrorCode.PLUGIN_ERROR +Exception: Failed to run "pub get" in the client project: +Resolving dependencies... + +Because every version of test_lint from path depends on dep any which doesn't exist (could not find package dep at https://pub.dev), test_lint from path is forbidden. +So, because custom_lint_client depends on test_lint from path, version solving failed. +''', + ), + ); + expect(process.exitCode, completion(1)); + }, + ); + }); + + group('Watch mode', () { + group('[q] quits', () { + test('with exit code 0 when no lints', () async { + final workspace = createTemporaryDirectory(); + + final process = await TestProcess.start( + 'dart', + [ + customLintBinPath, + '--watch', + ], + workingDirectory: workspace.path, + ); + + expect(await process.stdout.next, 'Analyzing...'); + await process.stdout.skip(1); + expect(await process.stdout.next, 'No issues found!'); + + await process.stdout.skip(3); + expect(await process.stdout.next, 'q: Quit'); + + process.stdin.write('q'); + + await expectLater(process.stdout.rest, emitsThrough(emitsDone)); + await process.shouldExit(0); + }); + + test('with exit code 1 when there are lints', () async { + final workspace = createTemporaryDirectory(); + + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + ), + ]), + ); + + createLintUsage( + parent: workspace, + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await TestProcess.start( + 'dart', + [ + customLintBinPath, + '--watch', + ], + workingDirectory: workspace.path, + ); + + expect( + await process.stdout.next, + startsWith('The Dart VM service is listening on'), + ); + expect( + await process.stdout.next, + startsWith('The Dart DevTools debugger and profiler is available at'), + ); + expect(await process.stdout.next, 'Analyzing...'); + await process.stdout.skip(1); + expect( + await process.stdout.next, + ' test_app/lib/main.dart:1:6 • Hello world • hello_world • INFO', + ); + await process.stdout.skip(1); + expect(await process.stdout.next, '1 issue found.'); + + await process.stdout.skip(3); + expect(await process.stdout.next, 'q: Quit'); + + process.stdin.write('q'); + + await expectLater(process.stdout.rest, emitsThrough(emitsDone)); + await process.shouldExit(1); + }); + }); + + group('[r] reloads', () { + test('with no lints', () async { + final workspace = createTemporaryDirectory(); + + final process = await TestProcess.start( + 'dart', + [ + customLintBinPath, + '--watch', + ], + workingDirectory: workspace.path, + ); + + expect(await process.stdout.next, 'Analyzing...'); + await process.stdout.skip(1); + expect(await process.stdout.next, 'No issues found!'); + + await process.stdout.skip(3); + expect(await process.stdout.next, 'q: Quit'); + + process.stdin.write('r'); + + // Skip empty lines + await process.stdout.skip(2); + expect(await process.stdout.next, 'Manual re-lint...'); + await process.stdout.skip(1); + expect(await process.stdout.next, 'No issues found!'); + + process.stdin.write('q'); + + await process.shouldExit(0); + }); + + test('with lints', () async { + final workspace = createTemporaryDirectory(); + + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + ), + ]), + ); + + createLintUsage( + parent: workspace, + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await TestProcess.start( + 'dart', + [ + customLintBinPath, + '--watch', + ], + workingDirectory: workspace.path, + ); + + expect( + await process.stdout.next, + startsWith('The Dart VM service is listening on'), + ); + expect( + await process.stdout.next, + startsWith('The Dart DevTools debugger and profiler is available at'), + ); + expect(await process.stdout.next, 'Analyzing...'); + await process.stdout.skip(1); + expect( + await process.stdout.next, + ' test_app/lib/main.dart:1:6 • Hello world • hello_world • INFO', + ); + await process.stdout.skip(1); + expect(await process.stdout.next, '1 issue found.'); + await process.stdout.skip(3); + expect(await process.stdout.next, 'q: Quit'); + + process.stdin.write('r'); + + // Skip empty lines + await process.stdout.skip(2); + expect(await process.stdout.next, 'Manual re-lint...'); + await process.stdout.skip(1); + expect( + await process.stdout.next, + ' test_app/lib/main.dart:1:6 • Hello world • hello_world • INFO', + ); + await process.stdout.skip(1); + expect(await process.stdout.next, '1 issue found.'); + + process.stdin.write('q'); + + await process.shouldExit(1); + }); + }); + }); + + group('--fix', () { + final fixedPlugin = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + onVariable: 'if (node.name.toString().endsWith("fixed")) return;', + fixes: [TestLintFix(name: 'OyFix')], + ), + ]); + + test('Applies possible fixes and return remaining lints', () async { + final fixedPlugin = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + onVariable: 'if (node.name.toString().endsWith("fixed")) return;', + fixes: [TestLintFix(name: 'OyFix')], + ), + ]); + + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + final plugin2 = createPlugin( + name: 'test_lint2', + main: helloWordPluginSource, + ); + + final app = createLintUsage( + name: 'test_app', + source: {'lib/main.dart': 'void fn() {}'}, + plugins: { + 'test_lint': plugin.uri, + 'test_lint2': plugin2.uri, + }, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath, '--fix'], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + expect( + app.file('lib', 'main.dart').readAsStringSync(), + 'void fnfixed() {}', + ); + + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Hello world • hello_world • INFO + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + + test('Supports adding imports', () async { + final fixedPlugin = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + onVariable: 'if (node.name.toString().endsWith("fixed")) return;', + fixes: [ + TestLintFix( + name: 'OyFix', + dartBuilderCode: r''' +builder.importLibrary(Uri.parse('package:path/path.dart')); + +builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed'); +''', + ), + ], + ), + ]); + + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + + final app = createLintUsage( + name: 'test_app', + source: { + 'lib/main.dart': ''' +void fn() {} +void fn2() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath, '--fix'], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + expect(app.file('lib', 'main.dart').readAsStringSync(), ''' +import 'package:path/path.dart'; + +void fnfixed() {} +void fn2fixed() {} +'''); + + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + + test('Can fix all lints', () async { + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + + final app = createLintUsage( + name: 'test_app', + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath, '--fix'], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + expect( + app.file('lib', 'main.dart').readAsStringSync(), + 'void fnfixed() {}', + ); + + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + + test( + 'Does not attempt at fixing the same lints again if a fix did not work', + () async { + final uselessFix = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + onVariable: 'if (node.name.toString().endsWith("fixed")) return;', + fixes: [TestLintFix(name: 'OyFix', nodeVisitor: '')], + ), + ]); + + final plugin = createPlugin(name: 'test_lint', main: uselessFix); + + final app = createLintUsage( + name: 'test_app', + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath, '--fix'], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Oy • oy • INFO + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + }); +} diff --git a/packages/custom_lint/test/cli_test.dart b/packages/custom_lint/test/cli_test.dart new file mode 100644 index 00000000..86ea9290 --- /dev/null +++ b/packages/custom_lint/test/cli_test.dart @@ -0,0 +1,597 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:custom_lint/src/output/output_format.dart'; +import 'package:test/test.dart'; + +import 'create_project.dart'; +import 'equals_ignoring_ansi.dart'; +import 'peer_project_meta.dart'; + +final oyPluginSource = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + ), +]); + +final helloWordPluginSource = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + ), +]); + +String jsonLints(String dir) { + return jsonEncode({ + 'version': 1, + 'diagnostics': [ + { + 'code': 'hello_world', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 9, + 'line': 1, + 'column': 10, + }, + }, + }, + 'problemMessage': 'Hello world', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 9, + 'line': 1, + 'column': 10, + }, + }, + }, + 'problemMessage': 'Oy', + }, + { + 'code': 'hello_world', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 7, + 'line': 1, + 'column': 8, + }, + }, + }, + 'problemMessage': 'Hello world', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 7, + 'line': 1, + 'column': 8, + }, + }, + }, + 'problemMessage': 'Oy', + } + ], + }); +} + +void main() { + // Run 2 tests, one with ANSI escapes and one without + // One test has no lints, the other has some, this should be enough. + for (final ansi in [true, false]) { + for (final format in OutputFormatEnum.values.map((e) => e.name)) { + group('With ANSI: $ansi and format: $format', () { + test('exits with 0 when no lint and no error are found', () async { + final plugin = createPlugin( + name: 'test_lint', + main: emptyPluginSource, + ); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = Process.runSync( + 'dart', + [ + customLintBinPath, + '--format', + format, + ], + workingDirectory: app.path, + stderrEncoding: utf8, + stdoutEncoding: utf8, + ); + + if (format == 'json') { + expect(process.stdout, ''' +{"version":1,"diagnostics":[]} +'''); + } else { + expect( + process.stdout, + ''' +Analyzing... + +No issues found! +''', + ); + } + + expect(process.stderr, isEmpty); + expect(process.exitCode, 0); + }); + + for (final installAsDevDependency in [false, true]) { + test( + 'CLI lists warnings from all plugins and set exit code installAsDevDependency: $installAsDevDependency', + () async { + final plugin = + createPlugin(name: 'test_lint', main: helloWordPluginSource); + final plugin2 = + createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + installAsDevDependency: installAsDevDependency, + ); + + final process = await Process.start( + 'dart', + [ + customLintBinPath, + '--format', + format, + ], + workingDirectory: app.path, + ); + + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect(err, emitsDone); + + if (format == 'json') { + expect( + out.join(), + completion( + equals('${jsonLints(app.resolveSymbolicLinksSync())}\n'), + ), + ); + } else { + expect( + out.join(), + completion( + allOf( + startsWith('Analyzing...'), + endsWith(''' + lib/another.dart:1:6 • Hello world • hello_world • INFO + lib/another.dart:1:6 • Oy • oy • INFO + lib/main.dart:1:6 • Hello world • hello_world • INFO + lib/main.dart:1:6 • Oy • oy • INFO + +4 issues found. +'''), + ), + ), + ); + } + expect(await process.exitCode, 1); + }); + } + }); + } + } + + test('exits with 1 if only an error but no lint are found', () async { + final plugin = createPlugin(name: 'test_lint', main: 'invalid;'); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + ); + + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + err.join(), + completion( + allOf([ + matchIgnoringAnsi(contains, ''' +/lib/test_lint.dart:1:1: Error: Variables must be declared using the keywords 'const', 'final', 'var' or a type name. +Try adding the name of the type of the variable or the keyword 'var'. +invalid; +^^^^^^^ +'''), + matchIgnoringAnsi(contains, ''' +lib/custom_lint_client.dart:16:29: Error: Undefined name 'createPlugin'. + {'test_lint': test_lint.createPlugin, + ^^^^^^^^^^^^ +'''), + ]), + ), + ); + expect(out, emitsDone); + expect(await process.exitCode, 1); + }); + + test('exits with 0 when pass argument `--no-fatal-infos`', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath, '--no-fatal-infos'], + workingDirectory: app.path, + ); + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + out.join(), + completion( + matchIgnoringAnsi(contains, ''' +Analyzing... + + lib/main.dart:1:6 • Hello world • hello_world • INFO + +1 issue found. +'''), + ), + ); + expect(err, emitsDone); + expect(await process.exitCode, 0); + }); + + test( + 'exits with 0 when found warning and pass argument `--no-fatal-warnings`', + () async { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + errorSeverity: ErrorSeverity.WARNING, + ), + ]), + ); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath, '--no-fatal-warnings'], + workingDirectory: app.path, + ); + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + out.join(), + completion( + matchIgnoringAnsi(contains, ''' +Analyzing... + + lib/main.dart:1:6 • Hello world • hello_world • WARNING + +1 issue found. +'''), + ), + ); + expect(err, emitsDone); + expect(await process.exitCode, 0); + }); + + test('supports plugins that do not compile', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + final plugin2 = createPlugin( + name: 'test_lint2', + main: "int x = 'oy';", + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + ); + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + err.join(), + completion( + allOf([ + matchIgnoringAnsi(contains, ''' +/lib/test_lint2.dart:1:9: Error: A value of type 'String' can't be assigned to a variable of type 'int'. +int x = 'oy'; + ^ +'''), + matchIgnoringAnsi(contains, ''' +lib/custom_lint_client.dart:18:26: Error: Undefined name 'createPlugin'. +'test_lint2': test_lint2.createPlugin, + ^^^^^^^^^^^^ +'''), + ]), + ), + ); + expect(out.join(), completion(isEmpty)); + expect(await process.exitCode, 1); + }); + + test('Shows prints and exceptions', () async { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + onVariable: r''' + if (node.name.lexeme == 'fail') { + print(''); + print(' '); + print('Hello\nworld'); + throw StateError('fail'); + } +''', + ), + ]), + ); + + final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + ); + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + out.join(), + completion( + allOf( + contains(''' +[hello_world] +[hello_world] +[hello_world] Hello +[hello_world] world +'''), + endsWith( + ''' +Analyzing... + + lib/another.dart:1:6 • Oy • oy • INFO + lib/main.dart:1:6 • Hello world • hello_world • INFO + lib/main.dart:1:6 • Oy • oy • INFO + +3 issues found. +''', + ), + ), + ), + ); + expect( + err.join(), + completion( + contains(''' +Plugin hello_world threw while analyzing ${app.resolveSymbolicLinksSync()}/lib/another.dart: +Bad state: fail +#0 hello_world.run. (package:test_lint/test_lint.dart:'''), + ), + ); + expect(await process.exitCode, 1); + }); + + test('Sorts lints by line then column the code', () async { + final plugin = createPlugin( + name: 'test_lint', + main: ''' +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +PluginBase createPlugin() => _HelloWorldLint(); + +class _HelloWorldLint extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => const [_Lint()]; +} + +class _Lint extends DartLintRule { + const _Lint() : super(code: const LintCode(name: 'a', problemMessage: 'a')); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + final line2 = resolver.lineInfo.getOffsetOfLine(1); + reporter.atOffset( + errorCode: const LintCode(name: 'x2', problemMessage: 'x2'), + offset: line2 + 1, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'a', problemMessage: 'a'), + offset: line2 + 1, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'x', problemMessage: 'x'), + offset: line2 + 1, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'y', problemMessage: 'y'), + offset: line2, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'z', problemMessage: 'z'), + offset: 0, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'w', problemMessage: 'w', errorSeverity: ErrorSeverity.WARNING), + offset: 0, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 'e', problemMessage: 'e', errorSeverity: ErrorSeverity.ERROR), + offset: 0, + length: 1, + ); + reporter.atOffset( + errorCode: const LintCode(name: 's', problemMessage: 's', errorSeverity: ErrorSeverity.ERROR), + offset: 1, + length: 2, + ); + } +} +''', + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void main() { + print('hello world'); +}''', + 'lib/other.dart': ''' +void other() { + print('hello other world'); +}''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final process = await Process.start( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + ); + final out = process.stdout.map(utf8.decode); + final err = process.stderr.map(utf8.decode); + + expect( + err.join(), + completion(isEmpty), + ); + + expect( + out.join(), + completion(''' +Analyzing... + + lib/main.dart:1:1 • e • e • ERROR + lib/main.dart:1:2 • s • s • ERROR + lib/other.dart:1:1 • e • e • ERROR + lib/other.dart:1:2 • s • s • ERROR + lib/main.dart:1:1 • w • w • WARNING + lib/other.dart:1:1 • w • w • WARNING + lib/main.dart:1:1 • z • z • INFO + lib/main.dart:2:1 • y • y • INFO + lib/main.dart:2:2 • a • a • INFO + lib/main.dart:2:2 • x • x • INFO + lib/main.dart:2:2 • x2 • x2 • INFO + lib/other.dart:1:1 • z • z • INFO + lib/other.dart:2:1 • y • y • INFO + lib/other.dart:2:2 • a • a • INFO + lib/other.dart:2:2 • x • x • INFO + lib/other.dart:2:2 • x2 • x2 • INFO + +16 issues found. +'''), + ); + expect(await process.exitCode, 1); + }); +} diff --git a/packages/custom_lint/test/create_project.dart b/packages/custom_lint/test/create_project.dart new file mode 100644 index 00000000..f1478097 --- /dev/null +++ b/packages/custom_lint/test/create_project.dart @@ -0,0 +1,382 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:path/path.dart'; +import 'package:test/scaffolding.dart'; + +import 'peer_project_meta.dart'; + +const _pluginDefaultPubspec = '<<>>'; + +const emptyPluginSource = ''' +import 'package:analyzer/dart/element/element.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/dart/analysis/results.dart'; + +PluginBase createPlugin() => _Plugin(); + +class _Plugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => []; +} +'''; + +class TestLintRule { + TestLintRule({ + required this.code, + required this.message, + this.startUp = '', + this.onRun = '', + this.onVariable = '', + this.ruleMembers = '', + this.fixes = const [], + this.errorSeverity = ErrorSeverity.INFO, + }); + + final String code; + final String message; + final String startUp; + final String onRun; + final String onVariable; + final String ruleMembers; + final List fixes; + final ErrorSeverity errorSeverity; + + void run(StringBuffer buffer) { + final fixesCode = fixes.isEmpty + ? '' + : ''' +@override +List getFixes() => [${fixes.map((e) => '${e.name}()').join(',')}]; +'''; + + for (final fix in fixes) { + fix.write(buffer, this); + } + + buffer.write(''' +class $code extends DartLintRule { + $code() + : super( + code: LintCode(name: '$code', + problemMessage: '$message', + errorSeverity: ErrorSeverity.${errorSeverity.displayName.toUpperCase()}), + ); + +$fixesCode +$ruleMembers +'''); + + if (startUp.isNotEmpty) { + buffer.write(''' + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async { + $startUp + } +'''); + } + + buffer.write( + ''' + @override + void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) { + $onRun + context.registry.addFunctionDeclaration((node) { + $onVariable + reporter.atToken(node.name, code); + }); + } +} +''', + ); + } +} + +class TestLintFix { + TestLintFix({ + required this.name, + this.nodeVisitor, + this.dartBuilderCode = + r"builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed');", + }); + + final String name; + final String? nodeVisitor; + final String? dartBuilderCode; + + void write(StringBuffer buffer, TestLintRule rule) { + buffer.write(''' +class $name extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addFunctionDeclaration((node) { + if (!analysisError.sourceRange.intersects(node.sourceRange)) return; + + final changeBuilder = reporter.createChangeBuilder( + priority: 1, + message: 'Fix ${rule.code}', + ); + + ${nodeVisitor ?? ''' + changeBuilder.addDartFileEdit((builder) { + $dartBuilderCode + }); +'''} + }); + } +} +'''); + } +} + +String createPluginSource(List rules) { + final buffer = StringBuffer(''' +import 'package:analyzer/dart/element/element.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:path/path.dart' as p; + +PluginBase createPlugin() => _Plugin(); + +class _Plugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ +'''); + + buffer.writeAll(rules.map((e) => '${e.code}()'), ','); + + buffer.write(']; }'); + + for (final rule in rules) { + rule.run(buffer); + } + + return buffer.toString(); +} + +Directory createPlugin({ + required String name, + Directory? parent, + String pubpsec = _pluginDefaultPubspec, + String? analysisOptions, + String? main, + Map? sources, + bool omitPackageConfig = false, + Map extraDependencies = const {}, +}) { + assert( + pubpsec == _pluginDefaultPubspec || extraDependencies.isEmpty, + 'Cannot specify both pubpsec and extraDependencies', + ); + + return createDartProject( + parent: parent, + sources: { + ...?sources, + if (main != null) join('lib', '$name.dart'): main, + }, + pubspec: pubpsec == _pluginDefaultPubspec + ? ''' +name: $name +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + analyzer: any + analyzer_plugin: any + custom_lint_builder: + path: ${PeerProjectMeta.current.customLintBuilderPath} +${extraDependencies.entries.map((e) => ' ${e.key}: ${e.value}').join('\n')} +''' + : pubpsec, + analysisOptions: analysisOptions, + packageConfig: omitPackageConfig || pubpsec != _pluginDefaultPubspec + ? null + : createPackageConfig(name: name), + name: name, + ); +} + +Directory createLintUsage({ + Directory? parent, + Map plugins = const {}, + Map source = const {}, + Map extraPackageConfig = const {}, + bool installAsDevDependency = true, + required String name, +}) { + final pluginDependencies = plugins.entries + .map( + (e) => ''' + ${e.key}: + path: ${e.value.toFilePath()} +''', + ) + .join('\n'); + + return createDartProject( + sources: source, + analysisOptions: ''' +analyzer: + plugins: + - custom_lint + +''', + pubspec: ''' +name: $name +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + analyzer: any + analyzer_plugin: any +${installAsDevDependency ? "" : pluginDependencies} + +dev_dependencies: + custom_lint: + path: ${PeerProjectMeta.current.customLintPath} +${installAsDevDependency ? pluginDependencies : ""} + +dependency_overrides: + custom_lint: + path: ${PeerProjectMeta.current.customLintPath} + custom_lint_core: + path: ${PeerProjectMeta.current.customLintCorePath} + custom_lint_builder: + path: ${PeerProjectMeta.current.customLintBuilderPath} +''', + packageConfig: createPackageConfig( + plugins: {...plugins, ...extraPackageConfig}, + name: name, + ), + name: name, + parent: parent, + ); +} + +String createPackageConfig({ + Map plugins = const {}, + required String name, +}) { + return const JsonEncoder.withIndent(' ').convert({ + ...PeerProjectMeta.current.exampleLintPackageConfig, + 'packages': [ + ...(PeerProjectMeta.current.exampleLintPackageConfig['packages']! + as List) + .cast>() + .where( + (e) => + e['name'] != 'custom_lint' && + e['name'] != 'custom_lint_example_lint' && + e['name'] != 'custom_lint_core' && + e['name'] != 'custom_lint_builder', + ), + for (final plugin in plugins.entries) + { + 'name': plugin.key, + 'rootUri': plugin.value.toString(), + 'packageUri': 'lib/', + 'languageVersion': '3.0', + }, + { + 'name': name, + 'rootUri': '../', + 'packageUri': 'lib/', + 'languageVersion': '3.0', + }, + { + 'name': 'custom_lint', + 'rootUri': 'file://${PeerProjectMeta.current.customLintPath}', + 'packageUri': 'lib/', + 'languageVersion': '3.0', + }, + // Custom lint builder is always a transitive dev dependency if it is used, + // so it will be in the package config + { + 'name': 'custom_lint_builder', + 'rootUri': 'file://${PeerProjectMeta.current.customLintBuilderPath}', + 'packageUri': 'lib/', + 'languageVersion': '3.0', + }, + // Custom lint core is always a transitive dev dependency if it is used, + // so it will be in the package config + { + 'name': 'custom_lint_core', + 'rootUri': 'file://${PeerProjectMeta.current.customLintCorePath}', + 'packageUri': 'lib/', + 'languageVersion': '3.0', + }, + ], + }); +} + +Directory createDartProject({ + Directory? parent, + String? analysisOptions, + String? pubspec, + String? packageConfig, + Map? sources, + required String name, +}) { + // TODO import .dart_tool/package_config.json by default for speed, avoiding unnecessary pub get + + return createTmpFolder( + parent: parent, + { + ...?sources, + if (analysisOptions != null) 'analysis_options.yaml': analysisOptions, + if (pubspec != null) 'pubspec.yaml': pubspec, + if (packageConfig != null) + join('.dart_tool', 'package_config.json'): packageConfig, + }, + name, + ); +} + +/// Creates a temporary folder with the given [files] and [name]. +/// +/// The folder will be automatically deleted after the pending test ends. +Directory createTmpFolder( + Map files, + String name, { + Directory? parent, +}) { + late Directory newFolder; + if (parent == null) { + newFolder = Directory.systemTemp.createTempSync(name); + } else { + newFolder = Directory(join(parent.path, name))..createSync(); + } + addTearDown(() => newFolder.deleteSync(recursive: true)); + + for (final fileEntry in files.entries) { + assert(isRelative(fileEntry.key), 'Only relative file paths are supported'); + + final file = File(join(newFolder.path, fileEntry.key)); + file.createSync(recursive: true); + file.writeAsStringSync(fileEntry.value); + } + + return newFolder; +} diff --git a/packages/custom_lint/test/equals_ignoring_ansi.dart b/packages/custom_lint/test/equals_ignoring_ansi.dart new file mode 100644 index 00000000..200bb06f --- /dev/null +++ b/packages/custom_lint/test/equals_ignoring_ansi.dart @@ -0,0 +1,70 @@ +import 'package:ansi_styles/extension.dart'; +import 'package:test/test.dart'; + +/// Returns a matcher which matches if the match argument is a string and +/// is equal to [value] after removing ansi codes +Matcher equalsIgnoringAnsi(String value) => _IsEqualIgnoringAnsi(null, value); + +Matcher matchIgnoringAnsi( + Matcher Function(String value) matcher, + String value, +) { + return _IsEqualIgnoringAnsi(matcher(value), value); +} + +class _IsEqualIgnoringAnsi extends Matcher { + _IsEqualIgnoringAnsi(this.matcher, this._value); + + static final Object _mismatchedValueKey = Object(); + static final Object _matcherKey = Object(); + + final Matcher? matcher; + final String _value; + + @override + bool matches(Object? object, Map matchState) { + final description = (object! as String).strip.replaceAll(';49m', ''); + final matcher = this.matcher; + + final isMatching = + matcher?.matches(description, matchState) ?? _value == description; + + if (!isMatching) matchState[_mismatchedValueKey] = description; + if (matcher != null) matchState[_matcherKey] = matcher; + return isMatching; + } + + @override + Description describe(Description description) { + return description.addDescriptionOf(_value).add(' ignoring ansi codes'); + } + + @override + Description describeMismatch( + Object? item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + if (matchState.containsKey(_mismatchedValueKey)) { + final actualValue = matchState[_mismatchedValueKey]! as String; + final matcher = matchState[_matcherKey] as Matcher?; + + // Leading whitespace is added so that lines in the multiline + // description returned by addDescriptionOf are all indented equally + // which makes the output easier to read for this case. + mismatchDescription.add('expected normalized value\n '); + + if (matcher != null) { + mismatchDescription.add('\nto match\n ').addDescriptionOf(matcher); + } else { + mismatchDescription.addDescriptionOf(_value); + } + + mismatchDescription.add('\nbut got\n '); + mismatchDescription.addDescriptionOf(actualValue); + } + + return mismatchDescription; + } +} diff --git a/packages/custom_lint/test/error_report_test.dart b/packages/custom_lint/test/error_report_test.dart new file mode 100644 index 00000000..2c3d1f18 --- /dev/null +++ b/packages/custom_lint/test/error_report_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:test/test.dart'; + +import 'cli_process_test.dart'; +import 'create_project.dart'; +import 'peer_project_meta.dart'; + +void main() { + group('Reports errors', () { + test('inside LintRule.startUp', () { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + startUp: "throw StateError('hello');", + ), + ]), + ); + + final app = createLintUsage( + name: 'test_app', + plugins: {'test_lint': plugin.uri}, + source: {'lib/main.dart': 'void fn() {}'}, + ); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect( + trimDependencyOverridesWarning(process.stderr), + startsWith(''' +Plugin hello_world threw while analyzing ${app.file('lib', 'main.dart').resolveSymbolicLinksSync()}: +Bad state: hello +#0 hello_world.startUp (package:test_lint/test_lint.dart:'''), + ); + expect(process.stdout, isEmpty); + expect(process.exitCode, 1); + }); + + test('inside post-run callbacks', () { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + startUp: ''' + context.addPostRunCallback(() { + throw StateError('hello'); + }); + context.addPostRunCallback(() { + throw StateError('hello2'); + }); + return super.startUp(resolver, context); + ''', + ), + ]), + ); + + final app = createLintUsage( + name: 'test_app', + plugins: {'test_lint': plugin.uri}, + source: {'lib/main.dart': 'void fn() {}'}, + ); + + final process = Process.runSync( + 'dart', + [customLintBinPath], + workingDirectory: app.path, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + expect( + trimDependencyOverridesWarning(process.stderr), + allOf( + contains( + ''' +Bad state: hello +#0 hello_world.startUp. (package:test_lint/test_lint.dart:''', + ), + contains( + ''' +Bad state: hello2 +#0 hello_world.startUp. (package:test_lint/test_lint.dart:''', + ), + ), + ); + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Hello world • hello_world • INFO + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + }); +} diff --git a/packages/custom_lint/test/expect_lint_test.dart b/packages/custom_lint/test/expect_lint_test.dart new file mode 100644 index 00000000..29ec11c7 --- /dev/null +++ b/packages/custom_lint/test/expect_lint_test.dart @@ -0,0 +1,214 @@ +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:async/async.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'create_project.dart'; +import 'run_plugin.dart'; + +final source = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + onVariable: 'if (node.name.lexeme == "ignore") return;', + ), + TestLintRule( + code: 'foo', + message: 'Foo', + onVariable: 'if (node.name.lexeme == "ignore") return;', + ), +]); + +void main() { + test('supports `// expect_lint: code`', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + source: { + 'lib/empty.dart': ''' +// a file with no lint in it + +// expect_lint: some_lint +void ignore() {} +''', + 'lib/main.dart': ''' +void fn() {} + +// expect_lint: hello_world, foo, unknown +void fn2() {} + +// expect_lint: hello_world +void fn3() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app); + final lints = await runner.getLints(reload: false).then((lints) { + final result = {}; + for (final lint in lints) { + final key = path.basename(lint.file); + expect(result[key], isNull); + result[key] = lint; + } + return result; + }); + + expect(lints.length, 2); + + expect( + lints['main.dart'], + predicate((value) { + expect(value.file, path.join(app.path, 'lib', 'main.dart')); + expect(value.errors.length, 4); + + expect(value.errors.first.code, 'hello_world'); + expect( + value.errors.first.location, + Location(value.file, 5, 2, 1, 6, endColumn: 8, endLine: 1), + ); + + expect(value.errors[1].code, 'foo'); + expect( + value.errors[1].location, + Location(value.file, 5, 2, 1, 6, endColumn: 8, endLine: 1), + ); + + expect(value.errors[2].code, 'foo'); + expect( + value.errors[2].location, + Location(value.file, 104, 3, 7, 6, endColumn: 9, endLine: 7), + ); + + expect(value.errors[3].code, 'unfulfilled_expect_lint'); + expect( + value.errors[3].message, + 'Expected to find the lint unknown on next line but none found.', + ); + expect( + value.errors[3].location, + Location(value.file, 48, 7, 3, 35, endColumn: 42, endLine: 3), + ); + + return true; + }), + ); + + expect( + lints['empty.dart'], + predicate((value) { + expect(value.file, path.join(app.path, 'lib', 'empty.dart')); + expect(value.errors.length, 1); + + expect(value.errors[0].code, 'unfulfilled_expect_lint'); + expect( + value.errors[0].message, + 'Expected to find the lint some_lint on next line but none found.', + ); + expect( + value.errors[0].location, + Location(value.file, 46, 9, 3, 17, endColumn: 26, endLine: 3), + ); + + return true; + }), + ); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect(plugin.log.existsSync(), false); + }); + + test('// expect_lint update when the source code changes', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + source: { + 'lib/empty.dart': ''' +// expect_lint: some_lint +void ignore() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app); + final lints = StreamQueue(runner.channel.lints); + + expect( + await lints.next, + predicate((value) { + expect(value.file, path.join(app.path, 'lib', 'empty.dart')); + expect(value.errors.length, 1); + + expect(value.errors[0].code, 'unfulfilled_expect_lint'); + expect( + value.errors[0].message, + 'Expected to find the lint some_lint on next line but none found.', + ); + expect( + value.errors[0].location, + Location(value.file, 16, 9, 1, 17, endColumn: 26, endLine: 1), + ); + + return true; + }), + ); + + final sourceFile = File(path.join(app.path, 'lib', 'empty.dart')); + + sourceFile.writeAsStringSync(''' +// Let's push the expect_lint a bit further down the file + +// expect_lint: some_lint +void ignore() {} +'''); + await runner.channel.sendRequest( + AnalysisHandleWatchEventsParams([ + WatchEvent(WatchEventType.MODIFY, sourceFile.path), + ]), + ); + + expect( + await lints.next, + predicate((value) { + expect(value.file, path.join(app.path, 'lib', 'empty.dart')); + expect(value.errors.length, 1); + + expect(value.errors[0].code, 'unfulfilled_expect_lint'); + expect( + value.errors[0].message, + 'Expected to find the lint some_lint on next line but none found.', + ); + expect( + value.errors[0].location, + Location(value.file, 75, 9, 3, 17, endColumn: 26, endLine: 3), + ); + + return true; + }), + ); + + expect(lints.rest, emitsDone); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect(plugin.log.existsSync(), false); + }); +} diff --git a/packages/custom_lint/test/fixes_test.dart b/packages/custom_lint/test/fixes_test.dart new file mode 100644 index 00000000..ba8567c5 --- /dev/null +++ b/packages/custom_lint/test/fixes_test.dart @@ -0,0 +1,330 @@ +import 'dart:io'; + +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'create_project.dart'; +import 'goldens.dart'; +import 'run_plugin.dart'; + +final fixlessPlugin = createPluginSource([ + TestLintRule(code: 'hello_world', message: 'Hello world'), +]); + +final fixedPlugin = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + fixes: [TestLintFix(name: 'HelloWorldFix')], + ), +]); + +final noChangeFixPlugin = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + fixes: [ + TestLintFix( + name: 'HelloWorldFix', + nodeVisitor: '', + ), + ], + ), +]); + +final multiChangeFixPlugin = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + fixes: [ + TestLintFix( + name: 'HelloWorldFix', + nodeVisitor: r''' + changeBuilder.addDartFileEdit( + (builder) { + builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed'); + }, + ); + changeBuilder.addDartFileEdit( + customPath: '${p.dirname(resolver.path)}/src/hello_world.dart', + (builder) { + builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed'); + }, + ); +''', + ), + ], + ), +]); + +const ignoreId = '<>'; + +void main() { + test('Can emit fixes', () async { + final plugin = createPlugin( + name: 'test_lint', + main: fixedPlugin, + ); + + const mainSource = ''' +void fn() {} + +void fn2() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = runner.getFixes(mainPath, 6); + final fixes2 = runner.getFixes(mainPath, 20); + + expectMatchesGoldenFixes( + [await fixes, await fixes2] + .expand((e) => e.fixes) + .expand((e) => e.fixes) + .where((e) => e.change.id != ignoreId), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'fixes.diff', + ), + ); + }); + + test('Fix-all is not present if a lint only has a single issue', () async { + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + + const mainSource = ''' +void fn() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = await runner.getFixes(mainPath, 6); + + expectMatchesGoldenFixes( + fixes.fixes.expand((e) => e.fixes).where((e) => e.change.id != ignoreId), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'single_fix.diff', + ), + ); + }); + + test('Fix-all does not apply to silenced lints', () async { + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + + const mainSource = ''' +void fn() {} + +void fn2() {} + +// ignore: hello_world +void fn3() {} + +// expect_lint: hello_world +void fn4() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = await runner.getFixes(mainPath, 6); + + expectMatchesGoldenFixes( + fixes.fixes.expand((e) => e.fixes).where((e) => e.change.id != ignoreId), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'silenced_change.diff', + ), + ); + }); + + test('Supports fixes with no changes', () async { + final plugin = createPlugin(name: 'test_lint', main: noChangeFixPlugin); + + const mainSource = ''' +void fn() {} + +void fn2() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = runner.getFixes(mainPath, 6); + final fixes2 = runner.getFixes(mainPath, 20); + + expectMatchesGoldenFixes( + [await fixes, await fixes2] + .expand((e) => e.fixes) + .expand((e) => e.fixes) + .where((e) => e.change.id != ignoreId), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'no_change.diff', + ), + ); + }); + + test('Supports fixes that emits multiple changes', () async { + final plugin = createPlugin(name: 'test_lint', main: multiChangeFixPlugin); + + const mainSource = ''' +void fn() {} + +void fn2() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = runner.getFixes(mainPath, 6); + final fixes2 = runner.getFixes(mainPath, 20); + + expectMatchesGoldenFixes( + [await fixes, await fixes2] + .expand((e) => e.fixes) + .expand((e) => e.fixes) + .where((e) => e.change.id != ignoreId), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'multi_change.diff', + ), + ); + }); + + test('Can add new ignores', () async { + final plugin = createPlugin(name: 'test_lint', main: fixlessPlugin); + + const mainSource = ''' +void fn() {} + +void fn2() {} + + void fn3() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = runner.getFixes(mainPath, 6); + final fixes2 = runner.getFixes(mainPath, 20); + final fixes3 = runner.getFixes(mainPath, 37); + + expectMatchesGoldenFixes( + [await fixes, await fixes2, await fixes3] + .expand((e) => e.fixes) + .expand((e) => e.fixes), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'add_ignore.diff', + ), + ); + }); + + test('Can update existing ignores', () async { + final plugin = createPlugin(name: 'test_lint', main: fixlessPlugin); + + const mainSource = ''' +// ignore: hello_world +void fn() {} + +// ignore_for_file: foo +// ignore: foo +void fn2() {} +'''; + final app = createLintUsage( + source: { + 'lib/main.dart': mainSource, + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + final mainPath = p.join(app.path, 'lib', 'main.dart'); + + final runner = await startRunnerForApp(app); + await runner.channel.lints.first; + + final fixes = runner.getFixes(mainPath, 30); + final fixes2 = runner.getFixes(mainPath, 84); + + expectMatchesGoldenFixes( + [await fixes, await fixes2].expand((e) => e.fixes).expand((e) => e.fixes), + sources: ({'**/*': mainSource}, relativePath: app.path), + file: Directory.current.file( + 'test', + 'goldens', + 'fixes', + 'update_ignore.diff', + ), + ); + }); +} diff --git a/packages/custom_lint/test/goldens.dart b/packages/custom_lint/test/goldens.dart new file mode 100644 index 00000000..cf2d026d --- /dev/null +++ b/packages/custom_lint/test/goldens.dart @@ -0,0 +1,187 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:collection/collection.dart'; +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +@Deprecated('do not commit') +void saveGoldensFixes( + Iterable fixes, { + (Map, {String relativePath})? sources, + required File file, +}) { + file.createSync(recursive: true); + + file.writeAsStringSync( + _encodePrioritizedSourceChanges( + fixes, + sources: sources?.$1, + relativePath: sources?.relativePath, + ), + ); +} + +void expectMatchesGoldenFixes( + Iterable fixes, { + (Map, {String relativePath})? sources, + required File file, +}) { + expect( + _encodePrioritizedSourceChanges( + fixes, + sources: sources?.$1, + relativePath: sources?.relativePath, + ), + file.readAsStringSync(), + ); +} + +// forked from custom-lint-core +String _encodePrioritizedSourceChanges( + Iterable changes, { + JsonEncoder? encoder, + Map? sources, + String? relativePath, +}) { + if (sources != null) { + final buffer = StringBuffer(); + + for (final prioritizedSourceChange in changes) { + buffer.writeln('Message: `${prioritizedSourceChange.change.message}`'); + buffer.writeln('Priority: ${prioritizedSourceChange.priority}'); + if (prioritizedSourceChange.change.selection case final selection?) { + buffer.writeln( + 'Selection: offset ${selection.offset} ; ' + 'file: `${selection.file}`; ' + 'length: ${prioritizedSourceChange.change.selectionLength}', + ); + } + + final files = prioritizedSourceChange.change.edits + .map((e) => p.normalize(p.relative(e.file, from: relativePath))) + .toSet() + .sortedBy((a) => a); + + for (final file in files) { + final source = sources.entries + .firstWhereOrNull( + (e) => + Glob(e.key).matches(file) || + // workaround to https://github.com/dart-lang/glob/issues/72 + Glob('/${e.key}').matches(file), + ) + ?.value; + if (source == null) { + throw StateError('No source found for file: $file'); + } + + final sourceLineInfo = LineInfo.fromContent(source); + + final output = SourceEdit.applySequence( + source, + prioritizedSourceChange.change.edits + .expand((element) => element.edits), + ); + + final outputLineInfo = LineInfo.fromContent(output); + + // Get the offset of the first changed character between output and source. + var firstDiffOffset = 0; + for (; firstDiffOffset < source.length; firstDiffOffset++) { + if (source[firstDiffOffset] != output[firstDiffOffset]) { + break; + } + } + + // Get the last changed character offset between output and source. + var endSourceOffset = source.length - 1; + var endOutputOffset = output.length - 1; + for (; + endOutputOffset > firstDiffOffset && + endSourceOffset > firstDiffOffset; + endOutputOffset--, endSourceOffset--) { + if (source[endSourceOffset] != output[endOutputOffset]) { + break; + } + } + + final firstChangedLine = + sourceLineInfo.getLocation(firstDiffOffset).lineNumber - 1; + + void writeDiff({ + required String file, + required LineInfo lineInfo, + required int endOffset, + required String token, + required int leadingCount, + required int trailingCount, + }) { + final lastChangedLine = + lineInfo.getLocation(endOffset).lineNumber - 1; + final endLine = + min(lastChangedLine + trailingCount, lineInfo.lineCount - 1); + for (var line = max(0, firstChangedLine - leadingCount); + line <= endLine; + line++) { + final changed = line >= firstChangedLine && line <= lastChangedLine; + if (changed) buffer.write(token); + + final endOfSource = !(line + 1 < lineInfo.lineCount); + + buffer.write( + file.substring( + lineInfo.getOffsetOfLine(line), + endOfSource ? null : lineInfo.getOffsetOfLine(line + 1) - 1, + ), + ); + if (!endOfSource) buffer.writeln(); + } + } + + buffer.writeln('Diff for file `$file:${firstChangedLine + 1}`:'); + buffer.writeln('```'); + writeDiff( + file: source, + lineInfo: sourceLineInfo, + endOffset: endSourceOffset, + leadingCount: 2, + trailingCount: 0, + token: '- ', + ); + + writeDiff( + file: output, + lineInfo: outputLineInfo, + endOffset: endOutputOffset, + leadingCount: 0, + trailingCount: 2, + token: '+ ', + ); + buffer.writeln('```'); + } + + buffer.writeln('---'); + } + + return buffer.toString(); + } + + final json = changes.map((e) => e.toJson()).toList(); + // Remove all "file" references from the json. + for (final change in json) { + final changeMap = change['change']! as Map; + final edits = changeMap['edits']! as List; + for (final edit in edits.cast>()) { + edit.remove('file'); + } + } + + encoder ??= const JsonEncoder.withIndent(' '); + return encoder.convert(json); +} diff --git a/packages/custom_lint/test/goldens/fixes/add_ignore.diff b/packages/custom_lint/test/goldens/fixes/add_ignore.diff new file mode 100644 index 00000000..b1f675ea --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/add_ignore.diff @@ -0,0 +1,68 @@ +Message: `Ignore "hello_world" for line` +Priority: 1 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ // ignore: hello_world ++ void fn() {} + +void fn2() {} +``` +--- +Message: `Ignore "hello_world" for file` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ // ignore_for_file: hello_world ++ void fn() {} + +void fn2() {} +``` +--- +Message: `Ignore "hello_world" for line` +Priority: 1 +Diff for file `lib/main.dart:3`: +``` +void fn() {} + +- void fn2() {} ++ // ignore: hello_world ++ void fn2() {} + + void fn3() {} +``` +--- +Message: `Ignore "hello_world" for file` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ // ignore_for_file: hello_world ++ void fn() {} + +void fn2() {} +``` +--- +Message: `Ignore "hello_world" for line` +Priority: 1 +Diff for file `lib/main.dart:5`: +``` +void fn2() {} + +- void fn3() {} ++ // ignore: hello_world ++ void fn3() {} +``` +--- +Message: `Ignore "hello_world" for file` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ // ignore_for_file: hello_world ++ void fn() {} + +void fn2() {} +``` +--- diff --git a/packages/custom_lint/test/goldens/fixes/fixes.diff b/packages/custom_lint/test/goldens/fixes/fixes.diff new file mode 100644 index 00000000..46ad1a9f --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/fixes.diff @@ -0,0 +1,44 @@ +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ void fnfixed() {} + +void fn2() {} +``` +--- +Message: `Fix all "hello_world"` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} +- +- void fn2() {} ++ void fnfixed() {} ++ ++ void fn2fixed() {} +``` +--- +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:3`: +``` +void fn() {} + +- void fn2() {} ++ void fn2fixed() {} +``` +--- +Message: `Fix all "hello_world"` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} +- +- void fn2() {} ++ void fnfixed() {} ++ ++ void fn2fixed() {} +``` +--- diff --git a/packages/custom_lint/test/goldens/fixes/multi_change.diff b/packages/custom_lint/test/goldens/fixes/multi_change.diff new file mode 100644 index 00000000..3fc490b1 --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/multi_change.diff @@ -0,0 +1,34 @@ +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ void fnfixedfixed() {} + +void fn2() {} +``` +Diff for file `lib/src/hello_world.dart:1`: +``` +- void fn() {} ++ void fnfixedfixed() {} + +void fn2() {} +``` +--- +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:3`: +``` +void fn() {} + +- void fn2() {} ++ void fn2fixedfixed() {} +``` +Diff for file `lib/src/hello_world.dart:3`: +``` +void fn() {} + +- void fn2() {} ++ void fn2fixedfixed() {} +``` +--- diff --git a/packages/custom_lint/test/goldens/fixes/no_change.diff b/packages/custom_lint/test/goldens/fixes/no_change.diff new file mode 100644 index 00000000..d7b05d42 --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/no_change.diff @@ -0,0 +1,12 @@ +Message: `Fix hello_world` +Priority: 1 +--- +Message: `Fix all "hello_world"` +Priority: 0 +--- +Message: `Fix hello_world` +Priority: 1 +--- +Message: `Fix all "hello_world"` +Priority: 0 +--- diff --git a/packages/custom_lint/test/goldens/fixes/silenced_change.diff b/packages/custom_lint/test/goldens/fixes/silenced_change.diff new file mode 100644 index 00000000..132b58b0 --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/silenced_change.diff @@ -0,0 +1,24 @@ +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ void fnfixed() {} + +void fn2() {} +``` +--- +Message: `Fix all "hello_world"` +Priority: 0 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} +- +- void fn2() {} ++ void fnfixed() {} ++ ++ void fn2fixed() {} + +// ignore: hello_world +``` +--- diff --git a/packages/custom_lint/test/goldens/fixes/single_fix.diff b/packages/custom_lint/test/goldens/fixes/single_fix.diff new file mode 100644 index 00000000..77960eb8 --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/single_fix.diff @@ -0,0 +1,8 @@ +Message: `Fix hello_world` +Priority: 1 +Diff for file `lib/main.dart:1`: +``` +- void fn() {} ++ void fnfixed() {} +``` +--- diff --git a/packages/custom_lint/test/goldens/fixes/update_ignore.diff b/packages/custom_lint/test/goldens/fixes/update_ignore.diff new file mode 100644 index 00000000..9805e57d --- /dev/null +++ b/packages/custom_lint/test/goldens/fixes/update_ignore.diff @@ -0,0 +1,23 @@ +Message: `Ignore "hello_world" for line` +Priority: 1 +Diff for file `lib/main.dart:5`: +``` + +// ignore_for_file: foo +- // ignore: foo ++ // ignore: foo, hello_world +void fn2() {} +``` +--- +Message: `Ignore "hello_world" for file` +Priority: 0 +Diff for file `lib/main.dart:4`: +``` +void fn() {} + +- // ignore_for_file: foo ++ // ignore_for_file: foo, hello_world +// ignore: foo +void fn2() {} +``` +--- diff --git a/packages/custom_lint/test/goldens/ignore_quick_fix.json b/packages/custom_lint/test/goldens/ignore_quick_fix.json new file mode 100644 index 00000000..16c0c663 --- /dev/null +++ b/packages/custom_lint/test/goldens/ignore_quick_fix.json @@ -0,0 +1,82 @@ +[ + { + "priority": 1, + "change": { + "message": "Ignore \"hello_world\" for line", + "edits": [ + { + "fileStamp": 0, + "edits": [ + { + "offset": 0, + "length": 0, + "replacement": "// ignore: hello_world\n" + } + ] + } + ], + "linkedEditGroups": [], + "id": "<>" + } + }, + { + "priority": 0, + "change": { + "message": "Ignore \"hello_world\" for file", + "edits": [ + { + "fileStamp": 0, + "edits": [ + { + "offset": 0, + "length": 0, + "replacement": "// ignore_for_file: hello_world\n" + } + ] + } + ], + "linkedEditGroups": [], + "id": "<>" + } + }, + { + "priority": 1, + "change": { + "message": "Ignore \"foo\" for line", + "edits": [ + { + "fileStamp": 0, + "edits": [ + { + "offset": 0, + "length": 0, + "replacement": "// ignore: foo\n" + } + ] + } + ], + "linkedEditGroups": [], + "id": "<>" + } + }, + { + "priority": 0, + "change": { + "message": "Ignore \"foo\" for file", + "edits": [ + { + "fileStamp": 0, + "edits": [ + { + "offset": 0, + "length": 0, + "replacement": "// ignore_for_file: foo\n" + } + ] + } + ], + "linkedEditGroups": [], + "id": "<>" + } + } +] \ No newline at end of file diff --git a/packages/custom_lint/test/goldens/server_test/redirect_logs.golden b/packages/custom_lint/test/goldens/server_test/redirect_logs.golden new file mode 100644 index 00000000..dbb3ec8b --- /dev/null +++ b/packages/custom_lint/test/goldens/server_test/redirect_logs.golden @@ -0,0 +1,4 @@ +[hello_world] 1990-01-01T00:00:00.000 Hello world +[hello_world] 1990-01-01T00:00:00.000 Plugin hello_world threw while analyzing app/lib/another.dart: +[hello_world] 1990-01-01T00:00:00.000 Bad state: fail +[hello_world] 1990-01-01T00:00:00.000 #0 hello_world.run. (package:test_lint/test_lint.dart:29:3) \ No newline at end of file diff --git a/packages/custom_lint/test/ignore_test.dart b/packages/custom_lint/test/ignore_test.dart new file mode 100644 index 00000000..b7998c56 --- /dev/null +++ b/packages/custom_lint/test/ignore_test.dart @@ -0,0 +1,254 @@ +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +import 'create_project.dart'; +import 'goldens.dart'; +import 'run_plugin.dart'; + +final source = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + ), + TestLintRule( + code: 'foo', + message: 'Foo', + ), +]); + +void main() { + test('Emits ignore/ignore_for_file quick-fixes', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + name: 'test_app', + plugins: {'test_lint': plugin.uri}, + source: { + 'lib/main.dart': ''' +void fn() {} +void fn2() {} +''', + }, + ); + + final runner = await startRunnerForApp(app); + await runner.getLints(reload: false); + final fixes = await runner + .getFixes(app.file('lib', 'main.dart').path, 6) + .then((e) => e.fixes); + + expect(fixes, hasLength(2)); + + expect(fixes[0].fixes, hasLength(2)); + expect(fixes[0].error.code, 'hello_world'); + expect( + fixes[0].fixes.map((e) => e.change.message), + unorderedEquals( + ['Ignore "hello_world" for line', 'Ignore "hello_world" for file'], + ), + ); + + expect(fixes[1].fixes, hasLength(2)); + expect(fixes[1].error.code, 'foo'); + expect( + fixes[1].fixes.map((e) => e.change.message), + unorderedEquals(['Ignore "foo" for line', 'Ignore "foo" for file']), + ); + + expectMatchesGoldenFixes( + fixes.expand((e) => e.fixes), + file: Directory.current.file('test', 'goldens', 'ignore_quick_fix.json'), + ); + }); + + test('Emits indented ignore quick-fix', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + name: 'test_app', + plugins: {'test_lint': plugin.uri}, + source: { + 'lib/main.dart': ''' + void fn() {} +''', + }, + ); + + final runner = await startRunnerForApp(app); + await runner.getLints(reload: false); + final fixes = await runner + .getFixes(app.file('lib', 'main.dart').path, 10) + .then((e) => e.fixes); + + expect( + fixes[0].fixes[0].change.edits[0].edits[0].replacement, + startsWith('${' ' * 4}// ignore: hello_world'), + ); + }); + + test('supports `// ignore: code`', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void fn() {} + +// ignore: hello_world, This is some comment foo +void fn2() {} + +// ignore: foo, hello_world +void fn3() {} + +// ignore: type=lint, some comment +void fn3() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app); + + expect( + await runner.channel.lints.first, + predicate((value) { + expect(value.file, join(app.path, 'lib', 'main.dart')); + expect(value.errors.length, 3); + + expect(value.errors.first.code, 'hello_world'); + expect( + value.errors.first.location, + Location(value.file, 5, 2, 1, 6, endColumn: 8, endLine: 1), + ); + + expect(value.errors[1].code, 'foo'); + expect( + value.errors[1].location, + Location(value.file, 5, 2, 1, 6, endColumn: 8, endLine: 1), + ); + + expect(value.errors[2].code, 'foo'); + expect( + value.errors[2].location, + Location(value.file, 68, 3, 4, 6, endColumn: 9, endLine: 4), + ); + return true; + }), + ); + + expect(runner.channel.lints, emitsDone); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect(plugin.log.existsSync(), false); + }); + + test('supports `// ignore_for_file: code`', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +// ignore_for_file: foo, some comment + +void fn() {} + +// ignore: hello_world +void fn2() {} + +// ignore: foo +void fn3() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app); + + expect( + await runner.channel.lints.first, + predicate((value) { + expect(value.file, join(app.path, 'lib', 'main.dart')); + expect(value.errors.length, 2); + + expect(value.errors.first.code, 'hello_world'); + expect( + value.errors.first.location, + Location(value.file, 44, 2, 3, 6, endColumn: 8, endLine: 3), + ); + + expect(value.errors[1].code, 'hello_world'); + expect( + value.errors[1].location, + Location(value.file, 111, 3, 9, 6, endColumn: 9, endLine: 9), + ); + return true; + }), + ); + + expect(runner.channel.lints, emitsDone); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect(plugin.log.existsSync(), false); + }); + + test('supports `// ignore_for_file: type=lint`', () async { + final plugin = createPlugin( + name: 'test_lint', + main: source, + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +// ignore_for_file: type=lint, some comment + +void fn() {} + +// ignore: hello_world +void fn2() {} + +// ignore: foo +void fn3() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app); + await runner.initialize; + + expect(runner.channel.lints, emitsDone); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect(plugin.log.existsSync(), false); + }); +} diff --git a/packages/custom_lint/test/matchers.dart b/packages/custom_lint/test/matchers.dart new file mode 100644 index 00000000..79e9c779 --- /dev/null +++ b/packages/custom_lint/test/matchers.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:test/test.dart'; + +final throwsAssertionError = throwsA(isAssertionError); + +final isAssertionError = isA(); + +Matcher matchesLogGolden( + String goldenPath, { + Map? paths, +}) { + return isA().having( + (e) => _normalizeLog(e, paths: paths), + 'normalized log matches golden file $goldenPath', + _normalizeLog(File(goldenPath).readAsStringSync(), paths: paths), + ); +} + +void saveLogGoldens( + File goldenPath, + String content, { + Map? paths, +}) { + goldenPath.createSync(recursive: true); + goldenPath.writeAsStringSync(_normalizeLog(content, paths: paths)); +} + +final _logDateRegex = RegExp(r'^\[(.+?)\] \S+', multiLine: true); + +String _normalizeLog(String log, {Map? paths}) { + var result = log.replaceAllMapped( + _logDateRegex, + (match) => '[${match.group(1)}] ${DateTime(1990).toIso8601String()}', + ); + + if (paths != null) { + for (final entry in paths.entries) { + result = result.replaceAll(entry.key.toString(), '${entry.value}/'); + result = result.replaceAll(entry.key.toFilePath(), '${entry.value}/'); + } + } + + return result; +} diff --git a/packages/custom_lint/test/mock_fs.dart b/packages/custom_lint/test/mock_fs.dart new file mode 100644 index 00000000..1a7e9ca3 --- /dev/null +++ b/packages/custom_lint/test/mock_fs.dart @@ -0,0 +1,153 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +/// Overrides the body of a test so that I/O is run against an in-memory +/// file system, not the host's disk. +/// +/// The I/O override is applied only to the code running within [testBody]. +Future runWithIOOverride( + FutureOr Function(Stream out, Stream err) testBody, { + Directory? currentDirectory, + bool supportsAnsiEscapes = false, +}) async { + final fs = _MockFs( + stdout, + currentDirectory ?? Directory.current, + supportsAnsiEscapes: supportsAnsiEscapes, + ); + + try { + return await IOOverrides.runWithIOOverrides( + () => testBody(fs.stdout.stream, fs.stderr.stream), + fs, + ); + } finally { + unawaited(fs.stderr.close()); + unawaited(fs.stdout.close()); + } +} + +class _StdoutOverride implements Stdout { + _StdoutOverride( + this._stdout, { + required this.supportsAnsiEscapes, + }); + + final Stdout _stdout; + + final _controller = StreamController(); + + Stream get stream => _controller.stream; + + @override + Encoding get encoding => _stdout.encoding; + + @override + set encoding(Encoding e) => throw UnimplementedError(); + + @override + void add(List data) => throw UnimplementedError(); + + @override + void addError(Object error, [StackTrace? stackTrace]) { + throw UnimplementedError(); + } + + @override + Future addStream(Stream> stream) => + throw UnimplementedError(); + + @override + Future close() => _controller.close(); + + @override + Future get done => throw UnimplementedError(); + + @override + Future flush() => throw UnimplementedError(); + + @override + bool get hasTerminal => _stdout.hasTerminal; + + @override + IOSink get nonBlocking => _stdout.nonBlocking; + + @override + final bool supportsAnsiEscapes; + + @override + int get terminalColumns => _stdout.terminalColumns; + + @override + int get terminalLines => _stdout.terminalLines; + + @override + void write(Object? object) { + _controller.add(object.toString()); + } + + @override + void writeAll(Iterable objects, [String sep = '']) { + _controller.add(objects.join(sep)); + } + + @override + void writeCharCode(int charCode) => throw UnimplementedError(); + + @override + void writeln([Object? object = '']) { + _controller.add('$object\n'); + } + + @override + String get lineTerminator => _stdout.lineTerminator; + + @override + set lineTerminator(String value) => _stdout.lineTerminator = value; +} + +/// Used to override file I/O with an in-memory file system for testing. +/// +/// Usage: +/// +/// ```dart +/// test('My FS test', withMockFs(() { +/// File('foo').createSync(); // File created in memory +/// })); +/// ``` +/// +/// Alternatively, set [IOOverrides.global] to a [_MockFs] instance in your +/// test's `setUp`, and to `null` in the `tearDown`. +class _MockFs extends IOOverrides { + _MockFs( + Stdout out, + this._directory, { + required bool supportsAnsiEscapes, + }) : stdout = _StdoutOverride(out, supportsAnsiEscapes: supportsAnsiEscapes), + stderr = _StdoutOverride(out, supportsAnsiEscapes: supportsAnsiEscapes); + + @override + final _StdoutOverride stdout; + + @override + final _StdoutOverride stderr; + + Directory _directory; + + @override + Directory getCurrentDirectory() => _directory; + + @override + FileSystemEntityType fseGetTypeSync(String path, bool followLinks) { + return Zone.current.parent!.run(() { + // Workaround to https://github.com/dart-lang/sdk/issues/54741 + return FileSystemEntity.typeSync(path, followLinks: followLinks); + }); + } + + @override + void setCurrentDirectory(String path) { + _directory = Directory(path); + } +} diff --git a/packages/custom_lint/test/peer_project_meta.dart b/packages/custom_lint/test/peer_project_meta.dart new file mode 100644 index 00000000..928b45d6 --- /dev/null +++ b/packages/custom_lint/test/peer_project_meta.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart'; + +final customLintBinPath = join( + PeerProjectMeta.current.customLintPath, + 'bin', + 'custom_lint.dart', +); + +class PeerProjectMeta { + PeerProjectMeta({ + required this.customLintPath, + required this.customLintBuilderPath, + required this.customLintCorePath, + required this.exampleAppPath, + required this.exampleLintPath, + required this.exampleLintPackageConfigString, + required this.exampleLintPackageConfig, + }) { + final dirs = [ + customLintPath, + customLintBuilderPath, + exampleAppPath, + exampleLintPath, + ]; + + for (final dir in dirs) { + if (!Directory(dir).existsSync()) { + throw StateError('Nothing found at $dir.'); + } + } + } + + factory PeerProjectMeta.fromDirectory(Directory directory) { + final exampleAppDir = Directory( + join( + directory.path, + 'example', + ), + ); + + final packagesPath = normalize(join(directory.path, '..')); + + final examplePackageConfigPath = join( + normalize(exampleAppDir.path), + 'example_lint', + '.dart_tool', + 'package_config.json', + ); + final exampleLintPackageConfigString = + File(examplePackageConfigPath).readAsStringSync(); + + return PeerProjectMeta( + customLintPath: join(packagesPath, 'custom_lint'), + customLintBuilderPath: join(packagesPath, 'custom_lint_builder'), + customLintCorePath: join(packagesPath, 'custom_lint_core'), + exampleAppPath: normalize(exampleAppDir.path), + exampleLintPath: + join(packagesPath, 'custom_lint', 'example', 'example_lint'), + exampleLintPackageConfigString: exampleLintPackageConfigString, + exampleLintPackageConfig: + jsonDecode(exampleLintPackageConfigString) as Map, + ); + } + + static final current = PeerProjectMeta.fromDirectory(Directory.current); + + final String customLintPath; + final String customLintBuilderPath; + final String customLintCorePath; + final String exampleAppPath; + final String exampleLintPath; + final String exampleLintPackageConfigString; + final Map exampleLintPackageConfig; +} diff --git a/packages/custom_lint/test/run_plugin.dart b/packages/custom_lint/test/run_plugin.dart new file mode 100644 index 00000000..b9382ea4 --- /dev/null +++ b/packages/custom_lint/test/run_plugin.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint/src/plugin_delegate.dart'; +import 'package:custom_lint/src/runner.dart'; +import 'package:custom_lint/src/server_isolate_channel.dart'; +import 'package:custom_lint/src/v2/custom_lint_analyzer_plugin.dart'; +import 'package:custom_lint/src/workspace.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +Future> runServerInCliModeForApp( + Directory directory, + + // to ignoreErrors as we cannot explicitly handle errors +) async { + final runner = await startRunnerForApp(directory, includeBuiltInLints: false); + return runner.runner.getLints(reload: false); +} + +class ManualRunner { + ManualRunner(this.runner, this.channel); + + final CustomLintRunner runner; + final ServerIsolateChannel channel; + + Future get initialize => runner.initialize; + + Future> getLints({required bool reload}) async { + return runner.getLints(reload: reload); + } + + Future getFixes( + String path, + int offset, + ) async { + return runner.getFixes(path, offset); + } + + Future close() async { + await runner.close(); + await channel.close(); + } +} + +Future startRunnerForApp( + Directory directory, { + bool ignoreErrors = false, + bool includeBuiltInLints = true, + bool watchMode = false, + bool fix = false, +}) async { + final zone = Zone.current; + final channel = ServerIsolateChannel(); + + final customLintServer = await CustomLintServer.start( + sendPort: channel.receivePort.sendPort, + workingDirectory: directory, + fix: fix, + delegate: CommandCustomLintDelegate(), + includeBuiltInLints: includeBuiltInLints, + watchMode: watchMode, + ); + + return CustomLintServer.runZoned(() => customLintServer, () async { + final workspace = await CustomLintWorkspace.fromPaths( + [directory.path], + workingDirectory: directory, + ); + final runner = CustomLintRunner(customLintServer, workspace, channel); + addTearDown(runner.close); + + if (!ignoreErrors) { + runner.channel + ..responseErrors.listen((event) { + zone.handleUncaughtError( + TestFailure( + '${event.message} ${event.code}\n${event.stackTrace}', + ), + StackTrace.current, + ); + }) + ..pluginErrors.listen((event) { + zone.handleUncaughtError( + TestFailure('${event.message}\n${event.stackTrace}'), + StackTrace.current, + ); + }); + } + + unawaited(runner.initialize); + + return ManualRunner(runner, channel); + }); +} + +extension LogFile on Directory { + File get log { + return File(p.join(path, 'custom_lint.log')); + } +} diff --git a/packages/custom_lint/test/server_test.dart b/packages/custom_lint/test/server_test.dart new file mode 100644 index 00000000..41ef8515 --- /dev/null +++ b/packages/custom_lint/test/server_test.dart @@ -0,0 +1,535 @@ +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:async/async.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +import 'cli_test.dart'; +import 'create_project.dart'; +import 'matchers.dart'; +import 'mock_fs.dart'; +import 'run_plugin.dart'; + +final lintRuleWithFilesToAnalyze = createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + ruleMembers: ''' +@override +List get filesToAnalyze => const ['test/*_test.dart']; +''', + ), +]); + +void main() { + test('List warnings for all files combined', () async { + final plugin = createPlugin( + name: 'test_lint', + main: lintRuleWithFilesToAnalyze, + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void fn() {} +''', + 'test/another.dart': 'void fn() {}\n', + 'test/another_test.dart': 'void fn() {}\n', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final rawLints = await runServerInCliModeForApp(app); + final lints = rawLints.where((e) => e.errors.isNotEmpty).toList(); + + expect( + lints.map((e) => e.file), + [join(app.path, 'test', 'another_test.dart')], + ); + + expect( + lints.single.errors.single, + isA() + .having((e) => e.code, 'code', 'hello_world') + .having( + (e) => e.location, + 'location', + Location( + join(app.path, 'test', 'another_test.dart'), + 5, + 2, + 1, + 6, + endLine: 1, + endColumn: 8, + ), + ) + .having((e) => e.message, 'message', 'Hello world'), + ); + }); + + test('Handles files getting deleted', () async { + // Regression test for https://github.com/invertase/dart_custom_lint/issues/105 + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void fn() {} + +void fn2() {} +''', + 'lib/another.dart': 'void fn() {}\n', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app, includeBuiltInLints: false); + final lints = await runner.getLints(reload: false); + + expect( + lints.map((e) => e.file), + [ + join(app.path, 'lib', 'another.dart'), + join(app.path, 'lib', 'main.dart'), + ], + ); + expect(lints[0].errors, hasLength(1)); + expect(lints[1].errors, hasLength(2)); + + final another = File(join(app.path, 'lib', 'another.dart')); + another.deleteSync(); + await runner.channel.sendRequest( + AnalysisUpdateContentParams({ + another.path: RemoveContentOverlay(), + }), + ); + + final lints2 = await runner.getLints(reload: true); + + expect( + lints2.map((e) => e.file), + [join(app.path, 'lib', 'main.dart')], + ); + expect(lints2[0].errors, hasLength(2)); + + await runner.close(); + }); + + test('List warnings for all files combined', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void fn() {} + +void fn2() {} +''', + 'lib/another.dart': 'void fn() {}\n', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final lints = await runServerInCliModeForApp(app); + + expect( + lints.map((e) => e.file), + [ + join(app.path, 'lib', 'another.dart'), + join(app.path, 'lib', 'main.dart'), + ], + ); + + expect( + lints.first.errors, + [ + isA() + .having((e) => e.code, 'code', 'hello_world') + .having( + (e) => e.location, + 'location', + Location( + join(app.path, 'lib', 'another.dart'), + 5, + 2, + 1, + 6, + endLine: 1, + endColumn: 8, + ), + ) + .having((e) => e.message, 'message', 'Hello world'), + ], + ); + expect( + lints.last.errors, + [ + isA() + .having((e) => e.code, 'code', 'hello_world') + .having( + (e) => e.location, + 'location', + Location( + join(app.path, 'lib', 'main.dart'), + 5, + 2, + 1, + 6, + endLine: 1, + endColumn: 8, + ), + ) + .having((e) => e.message, 'message', 'Hello world'), + isA() + .having((e) => e.code, 'code', 'hello_world') + .having( + (e) => e.location, + 'location', + Location( + join(app.path, 'lib', 'main.dart'), + 19, + 3, + 3, + 6, + endLine: 3, + endColumn: 9, + ), + ) + .having((e) => e.message, 'message', 'Hello world'), + ], + ); + }); + + test('supports running multiple plugins at once', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' + + +void fn() {}''', + 'lib/another.dart': ''' + + +void fn2() {}''', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + final lints = await runServerInCliModeForApp(app); + + expect( + lints.map((e) => e.file), + [ + join(app.path, 'lib', 'another.dart'), + join(app.path, 'lib', 'main.dart'), + ], + ); + + expect( + lints.first.errors.map((e) => e.code), + unorderedEquals(['hello_world', 'oy']), + ); + expect( + lints.last.errors.map((e) => e.code), + unorderedEquals(['hello_world', 'oy']), + ); + }); + + test('supports plugins without .package_config.json', () async { + final plugin = createPlugin( + name: 'test_lint', + main: helloWordPluginSource, + omitPackageConfig: true, + ); + final plugin2 = createPlugin( + name: 'test_lint2', + main: oyPluginSource, + omitPackageConfig: true, + ); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' + + +void fn() {}''', + 'lib/another.dart': ''' + + +void fn2() {}''', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + final lints = await runServerInCliModeForApp(app); + + expect( + lints.map((e) => e.file), + [ + join(app.path, 'lib', 'another.dart'), + join(app.path, 'lib', 'main.dart'), + ], + ); + + expect( + lints.first.errors.map((e) => e.code), + unorderedEquals(['hello_world', 'oy']), + ); + expect( + lints.last.errors.map((e) => e.code), + unorderedEquals(['hello_world', 'oy']), + ); + }); + + test('redirect prints and errors to log files', () async { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + onVariable: ''' +if (node.name.lexeme == "fail") { + print('Hello world'); + throw StateError('fail'); +}''', + ), + ]), + ); + final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': 'void fn() {}\n', + 'lib/another.dart': 'void fail() {}\n', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + await runWithIOOverride(currentDirectory: app, (out, err) async { + final runner = await startRunnerForApp( + app, + // Ignoring errors as we are handling them later + ignoreErrors: true, + ); + + // Plugin errors will be emitted as notifications, not as part of the response + expect(runner.channel.responseErrors, emitsDone); + + // The error in our plugin will be reported as PluginErrorParams + expect( + runner.channel.pluginErrors.toList(), + completion([ + predicate((value) { + expect( + value.message, + 'Plugin hello_world threw while analyzing ${app.path}/lib/another.dart:\n' + 'Bad state: fail', + ); + return true; + }), + ]), + ); + + final lints = await runner.getLints(reload: false); + + expect( + lints.map((e) => e.file), + [ + join(app.path, 'lib', 'another.dart'), + join(app.path, 'lib', 'main.dart'), + ], + ); + + expect( + lints.first.errors.map((e) => e.code), + unorderedEquals(['custom_lint_get_lint_fail', 'oy']), + ); + expect( + lints.last.errors.map((e) => e.code), + unorderedEquals(['hello_world', 'oy']), + ); + // uncomment when the golden needs update + // saveLogGoldens( + // File('test/goldens/server_test/redirect_logs.golden'), + // app.log.readAsStringSync().split('\n').take(4).join('\n'), + // paths: {plugin.uri: 'plugin', app.uri: 'app'}, + // ); + // await runner.close(); + // return; + + expect( + app.log.readAsStringSync().split('\n').take(4).join('\n'), + matchesLogGolden( + 'test/goldens/server_test/redirect_logs.golden', + paths: {plugin.uri: 'plugin', app.uri: 'app'}, + ), + ); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + }); + }); + + group('hot-reload', () { + test( + timeout: const Timeout.factor(2), + 'Supports starting custom_lint twice in watch mode at once', () async { + final plugin = createPlugin( + name: 'test_lint', + main: helloWordPluginSource, + ); + final pluginMain = File(join(plugin.path, 'lib', 'test_lint.dart')); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}\n'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + await runWithIOOverride((out, err) async { + final runner = await startRunnerForApp(app, watchMode: true); + final lints = StreamQueue(runner.channel.lints); + + final runner2 = await startRunnerForApp(app, watchMode: true); + final lints2 = StreamQueue(runner.channel.lints); + + expect( + await lints.next.then((value) => value.errors.map((e) => e.code)), + ['hello_world'], + ); + expect( + await lints2.next.then((value) => value.errors.map((e) => e.code)), + ['hello_world'], + ); + + pluginMain.writeAsStringSync( + createPluginSource([ + TestLintRule( + code: 'hello_reload', + message: 'Hello reload', + ), + ]), + flush: true, + ); + + expect( + await lints.next.then((value) => value.errors.map((e) => e.code)), + ['hello_reload'], + ); + expect( + await lints2.next.then((value) => value.errors.map((e) => e.code)), + ['hello_reload'], + ); + + await runner.close(); + await runner2.close(); + }); + }); + + test('handles the source change of one plugin and restart it', () async { + final plugin = createPlugin( + name: 'test_lint', + main: helloWordPluginSource, + ); + final pluginMain = File(join(plugin.path, 'lib', 'test_lint.dart')); + + final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}\n'}, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + await runWithIOOverride((out, err) async { + final runner = await startRunnerForApp(app, watchMode: true); + final lints = StreamQueue(runner.channel.lints); + + expect(err, emitsDone); + + expect( + await lints.next.then((value) => value.errors.map((e) => e.code)), + ['hello_world', 'oy'], + ); + + pluginMain.writeAsStringSync( + createPluginSource([ + TestLintRule( + code: 'hello_reload', + message: 'Hello reload', + ), + ]), + ); + + expect( + await lints.next.then((value) => value.errors.map((e) => e.code)), + ['hello_reload', 'oy'], + ); + + expect(lints.rest, emitsDone); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + expect( + app.log.readAsStringSync(), + matches( + RegExp(''' +The Dart VM service is listening on .+?=/ +The Dart DevTools debugger and profiler is available at: .+?ws +'''), + ), + ); + }); + }); + + test('is disabled if watch mode is off', () async { + final plugin = createPlugin( + name: 'test_lint', + main: helloWordPluginSource, + ); + + final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}\n'}, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + await runWithIOOverride((out, err) async { + final runner = await startRunnerForApp(app); + final lints = await runner.getLints(reload: false); + + expect(err, emitsDone); + + expect( + lints.single.errors.map((e) => e.code), + ['hello_world', 'oy'], + ); + + // Closing so that previous error matchers relying on stream + // closing can complete + await runner.close(); + + // Check that there is no vm-service-uri in the logs + expect(app.log.existsSync(), false); + }); + }); + }); +} diff --git a/packages/custom_lint/test/src/workspace_test.dart b/packages/custom_lint/test/src/workspace_test.dart new file mode 100644 index 00000000..62a8f158 --- /dev/null +++ b/packages/custom_lint/test/src/workspace_test.dart @@ -0,0 +1,3076 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint/src/workspace.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:test/test.dart'; + +/// Shorthand for calling [CustomLintWorkspace.fromContextRoots] from +/// a list of path. +Future fromContextRootsFromPaths( + List paths, { + required Directory workingDirectory, +}) { + return CustomLintWorkspace.fromContextRoots( + paths.map((path) { + return ContextRoot( + p.isAbsolute(path) ? path : p.join(workingDirectory.path, path), + [], + ); + }).toList(), + workingDirectory: workingDirectory, + ); +} + +extension on Dependency { + Map toPackageJson({ + required String name, + required String rootUri, + }) { + return { + 'name': name, + 'rootUri': rootUri, + 'packageUri': 'lib/', + 'languageVersion': '2.12', + }; + } + + Object? toJson() { + final that = this; + if (that is HostedDependency) { + if (that.hosted != null) { + String? safeName; + try { + safeName = that.hosted!.name; + + // `that.hosted!.name` could throw an error if `_nameOfPackage` is null in the getter. + // We need to safely handle this scenario because we can't guarantee that the value is not null. + // ignore: avoid_catching_errors + } on Error catch (_) {} + + return { + 'hosted': { + if (safeName != null) 'name': safeName, + 'url': that.hosted!.url.toString(), + }, + 'version': that.version.toString(), + }; + } + return that.version.toString(); + } else if (that is GitDependency) { + return { + 'git': { + 'url': that.url.toString(), + if (that.path != null) 'path': that.path, + if (that.ref != null) 'ref': that.ref, + }, + }; + } else if (that is PathDependency) { + return { + 'path': that.path, + }; + } else if (that is SdkDependency) { + return { + 'sdk': that.sdk, + }; + } else { + throw ArgumentError.value(that, 'dependency', 'Unknown dependency'); + } + } +} + +Queue<({String executable, List args, bool runInShell})> spyProcess() { + final result = + Queue<({String executable, List args, bool runInShell})>(); + + final previousRunProcess = runProcess; + addTearDown(() => runProcess = previousRunProcess); + runProcess = ( + executable, + arguments, { + environment, + includeParentEnvironment = true, + runInShell = false, + stderrEncoding, + stdoutEncoding, + workingDirectory, + }) async { + result + .add((executable: executable, args: arguments, runInShell: runInShell)); + + return previousRunProcess( + executable, + arguments, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + workingDirectory: workingDirectory, + ); + }; + + return result; +} + +extension on Pubspec { + Map toJson() { + return { + 'name': name, + if (version != null) 'version': version.toString(), + 'environment': { + for (final env in environment.entries) + if (env.value != null) env.key: env.value.toString(), + }, + if (dependencies.isNotEmpty) + 'dependencies': { + for (final dependency in dependencies.entries) + dependency.key: dependency.value.toJson(), + }, + if (devDependencies.isNotEmpty) + 'dev_dependencies': { + for (final dependency in devDependencies.entries) + dependency.key: dependency.value.toJson(), + }, + if (dependencyOverrides.isNotEmpty) + 'dependency_overrides': { + for (final dependency in dependencyOverrides.entries) + dependency.key: dependency.value.toJson(), + }, + }; + } +} + +Future createProject( + Directory dir, + Pubspec pubspec, { + required List> packageConfigs, +}) async { + // Write the pubpsec.yaml + final pubspecFile = dir.pubspec; + pubspecFile.createSync(recursive: true); + pubspecFile.writeAsStringSync(json.encode(pubspec.toJson())); + + // Write a package_config.json matching the dependencies + final packageConfigFile = dir.packageConfig; + packageConfigFile.createSync(recursive: true); + packageConfigFile.writeAsStringSync( + json.encode({ + 'configVersion': 2, + 'packages': packageConfigs, + }), + ); + + return dir; +} + +/// A simplified [createWorkspace] which alleviates the need to specify +/// both the path and the pubspec. +/// +/// This receives a list of either [String] or [Pubspec] objects. +/// - If a [String] is passed, it must be a path, and a project +/// with the name of the basename of the path will be created at that location. +/// - If a [Pubspec] is passed, a project will be created in a folder at the root +/// of the workspace with the project name. +/// If two packages have the same name, the second one will be suffixed with +/// an incrementing number. Such that we have `package`, `package2`, ... +Future createSimpleWorkspace( + List projectEntry, { + bool withPackageConfig = true, + bool local = false, +}) async { + /// The number of time we've created a package with a given name. + final packageCount = {}; + + String getFolderName(String name) { + // If a package with the same name was already created previously, + // we suffix the folder name with an incrementing number. + final projectFolderSuffix = + packageCount[name] == null ? '' : packageCount[name]!.toString(); + final folderName = '$name$projectFolderSuffix'; + + // Increment the counter for changing the suffix for the next similarly + // named package + packageCount[name] = (packageCount[name] ?? 1) + 1; + + return folderName; + } + + return createWorkspace(local: local, withPackageConfig: withPackageConfig, { + for (final projectEntry in projectEntry) + if (projectEntry is Pubspec) + getFolderName(projectEntry.name): projectEntry + else if (projectEntry is String) + getFolderName(projectEntry): Pubspec( + p.basename(projectEntry), + version: Version(1, 0, 0), + environment: { + 'sdk': VersionConstraint.parse('>=3.0.0 <4.0.0'), + }, + ) + else + // https://github.com/dart-lang/language/issues/2943 + 'foo': throw ArgumentError.value( + projectEntry, + 'projectEntry', + 'Expected either a String or a Pubspec', + ), + }); +} + +/// Create a temporary mono-repository setup with package_configs and pubspecs. +Future createWorkspace( + Map pubspecs, { + bool withPackageConfig = true, + bool local = false, +}) async { + final dir = createTemporaryDirectory(local: local); + + String packagePathOf(Dependency dependency, String name) { + switch (dependency) { + case PathDependency(): + return p.isAbsolute(dependency.path) + ? dependency.path + : p.normalize(p.join('..', dependency.path)); + case _: + return p.join(dir.path, name); + } + } + + await Future.wait([ + for (final pubspecEntry in pubspecs.entries) + Future(() async { + // Create the package directory + final projectDirectory = Directory( + p.normalize(p.absolute(dir.path, pubspecEntry.key)), + ); + projectDirectory.createSync(recursive: true); + + await createProject( + projectDirectory, + pubspecEntry.value, + packageConfigs: !withPackageConfig + ? const [] + : [ + for (final dependency + in pubspecEntry.value.dependencies.entries) + if (!pubspecEntry.value.dependencyOverrides.keys.contains( + dependency.key, + )) + dependency.value.toPackageJson( + name: dependency.key, + rootUri: + packagePathOf(dependency.value, dependency.key), + ), + for (final dependency + in pubspecEntry.value.devDependencies.entries) + if (!pubspecEntry.value.dependencyOverrides.keys + .contains(dependency.key) && + !pubspecEntry.value.dependencies.keys + .contains(dependency.key)) + dependency.value.toPackageJson( + name: dependency.key, + rootUri: + packagePathOf(dependency.value, dependency.key), + ), + for (final dependency + in pubspecEntry.value.dependencyOverrides.entries) + dependency.value.toPackageJson( + name: dependency.key, + rootUri: packagePathOf(dependency.value, dependency.key), + ), + ], + ); + }), + ]); + + return dir; +} + +Directory createTemporaryDirectory({bool local = false}) { + final Directory dir; + if (local) { + // The cli_process_test needs it to be local in order for the relative paths to match + dir = + Directory.current.dir('.dart_tool').createTempSync('custom_lint_test'); + } else { + // Others need global directory in order to not pick up this project's package_config.json + dir = Directory.systemTemp.createTempSync('custom_lint_test'); + } + addTearDown(() => dir.deleteSync(recursive: true)); + + // Watches process kill to delete the temporary directory. + late final StreamSubscription subscription; + subscription = ProcessSignal.sigint.watch().listen((_) { + dir.deleteSync(recursive: true); + // Let the process exit normally. + unawaited(subscription.cancel()); + }); + + return dir; +} + +void writeFile(File file, String content) { + file.createSync(recursive: true); + file.writeAsStringSync(content); +} + +const analysisOptionsWithCustomLintEnabled = ''' +analyzer: + plugins: + - custom_lint +'''; + +const analysisOptionsWithCustomLintDisabled = ''' +analyzer: + plugins: + - unrelated_plugin +'''; + +void main() { + group('visitAnalysisOptionAndIncludes', () { + test('returns empty stream if the analysis options file does not exist', + () async { + final dir = createTemporaryDirectory(); + final stream = visitAnalysisOptionAndIncludes(dir.analysisOptions); + + await expectLater(stream, emitsDone); + }); + + test('returns empty stream if the analysis options file is not a YAML map', + () async { + final dir = createTemporaryDirectory(); + writeFile(dir.analysisOptions, '42'); + + await expectLater( + visitAnalysisOptionAndIncludes(dir.analysisOptions), + emitsDone, + ); + }); + + test('Emits a YAML map from the analysis options file', () async { + final dir = createTemporaryDirectory(); + writeFile(dir.analysisOptions, 'foo: bar'); + + await expectLater( + visitAnalysisOptionAndIncludes(dir.analysisOptions), + emitsInOrder([ + {'foo': 'bar'}, + emitsDone, + ]), + ); + }); + + test('resolves an include path relative to the analysis options file', + () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + final includedFile = analysisOptions.parent.file('other.yaml'); + writeFile(analysisOptions, 'include: other.yaml'); + writeFile(includedFile, 'foo: bar'); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': 'other.yaml'}, + {'foo': 'bar'}, + ]), + ); + }); + + test( + 'handles nested package imports, ' + 'picking up dependencies from the root package config', + () async { + final dir = createTemporaryDirectory(); + + const packageName = 'foo'; + final packageDir = dir.dir('packages/$packageName'); + writeFile( + packageDir.file('lib', 'other.yaml'), + 'foo: bar', + ); + + const package2Name = 'bar'; + final package2Dir = dir.dir('packages/$package2Name'); + writeFile( + package2Dir.file('lib', 'src', 'file.yaml'), + 'include: package:$packageName/other.yaml', + ); + + final analysisOptions = dir.analysisOptions; + writeFile( + analysisOptions, + 'include: package:$package2Name/src/file.yaml', + ); + + final packageConfig = ''' + { + "configVersion":2, + "packages":[ + { + "name":"$packageName", + "rootUri":"file://${packageDir.path}", + "packageUri":"lib/" + }, + { + "name":"$package2Name", + "rootUri":"file://${package2Dir.path}", + "packageUri":"lib/" + } + ] + } + '''; + writeFile(dir.packageConfig, packageConfig); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': 'package:bar/src/file.yaml'}, + {'include': 'package:foo/other.yaml'}, + {'foo': 'bar'}, + emitsDone, + ]), + ); + }, + ); + + test( + 'handles include paths with the "package" scheme, ' + 'but package config does not contain the package', () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + final packageConfigFile = dir.packageConfig; + writeFile(analysisOptions, 'include: package:foo/other.yaml'); + + const packageConfig = ''' + { + "configVersion":2, + "packages":[{ + "name":"bar", + "rootUri":"file:///path/to/packages/bar", + "packageUri":"lib/" + }] + } + '''; + writeFile(packageConfigFile, packageConfig); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': 'package:foo/other.yaml'}, + // The "other.yaml" file was not visited because we could not resolve + // the import. + emitsDone, + ]), + ); + }); + + test( + 'handles include paths with the "package" scheme, ' + 'but package config does not exist', () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + writeFile(analysisOptions, 'include: package:foo/other.yaml'); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': 'package:foo/other.yaml'}, + emitsDone, + ]), + ); + }); + + test('handles includes with relative paths', () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + final includedFile = dir.file('other.yaml'); + writeFile(analysisOptions, 'include: other.yaml'); + writeFile(includedFile, 'foo: bar'); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': 'other.yaml'}, + {'foo': 'bar'}, + emitsDone, + ]), + ); + }); + + test('handles includes with absolute paths', () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + final includedFile = dir.file('other.yaml'); + writeFile(analysisOptions, 'include: ${includedFile.path}'); + writeFile(includedFile, 'foo: bar'); + + await expectLater( + visitAnalysisOptionAndIncludes(analysisOptions), + emitsInOrder([ + {'include': includedFile.path}, + {'foo': 'bar'}, + emitsDone, + ]), + ); + }); + + test('handles nested relative include paths', () async { + final dir = createTemporaryDirectory(); + final analysisOptions = dir.analysisOptions; + writeFile(analysisOptions, 'include: dir/included.yaml'); + + final includedFile = dir.file('dir', 'included.yaml'); + // The relative path is based on the location of includedFile + // rather than "analysisOptions". + writeFile(includedFile, 'include: ../dir2/other.yaml'); + + final otherFile = dir.file('dir2', 'other.yaml'); + writeFile(otherFile, 'baz: qux'); + + final stream = visitAnalysisOptionAndIncludes(analysisOptions); + await expectLater( + stream, + emitsInOrder([ + {'include': 'dir/included.yaml'}, + {'include': '../dir2/other.yaml'}, + {'baz': 'qux'}, + emitsDone, + ]), + ); + }); + + test('handles circular includes by throwing a CyclicIncludeException', + () async { + final dir = createTemporaryDirectory(); + + writeFile(dir.analysisOptions, 'include: other.yaml'); + writeFile(dir.file('other.yaml'), 'include: analysis_options.yaml'); + + await expectLater( + visitAnalysisOptionAndIncludes(dir.analysisOptions), + emitsInOrder([ + {'include': 'other.yaml'}, + {'include': 'analysis_options.yaml'}, + emitsError(isA()), + ]), + ); + }); + + test('handles errors in loading package config files', () async { + final dir = createTemporaryDirectory(); + + writeFile(dir.analysisOptions, 'include: package:foo/other.yaml'); + writeFile(dir.file('other.yaml'), 'foo: bar'); + writeFile(dir.packageConfig, 'invalid json'); + + await expectLater( + visitAnalysisOptionAndIncludes(dir.analysisOptions), + emitsInOrder([ + {'include': 'package:foo/other.yaml'}, + // The "other.yaml" file was not visited because we could not resolve + // the package config. + emitsDone, + ]), + ); + }); + }); + + group(CustomLintWorkspace, () { + group('computePuspecOverrides', () { + test('Do not generate a pubspec_overrides if none specified', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspecOverride(), null); + }); + + test('Merges dependendency_overrides', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + workingDir.dir('a').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + package: ">=1.1.0 <1.9.0" +'''); + + workingDir.dir('b').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + package: ">=1.0.0 <1.6.0" +'''); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspecOverride(), ''' +dependency_overrides: + package: ">=1.1.0 <1.6.0" +'''); + }); + + test('Throws on incompatible constraints', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + workingDir.dir('a').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + package: ">=1.1.0 <1.2.0" +'''); + + workingDir.dir('b').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + package: ">=1.3.0" +'''); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspecOverride, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "package" has incompatible version constraints in the project: +- ">=1.1.0 <1.2.0" + from "a" at "./a/pubspec_overrides.yaml". +- ">=1.3.0" + from "b" at "./b/pubspec_overrides.yaml". +'''), + ), + ); + }); + }); + + group('resolvePluginHost', () { + test('Does not write pubspec_overrides if none present', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + final tempDir = createTemporaryDirectory(); + await workspace.resolvePluginHost(tempDir); + + expect(tempDir.pubspec.existsSync(), true); + expect(tempDir.pubspecOverrides.existsSync(), false); + }); + + test('writes pubspecs & pubspec_overrides', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + workingDir.dir('a').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + plugin1: + path: "../plugin1" +'''); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + final tempDir = createTemporaryDirectory(); + await workspace.resolvePluginHost(tempDir); + + expect(tempDir.pubspec.readAsStringSync(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + plugin1: + path: "${p.posix.prettyUri(workingDir.dir('plugin1').path)}" +'''); + expect(tempDir.pubspecOverrides.readAsStringSync(), ''' +dependency_overrides: + plugin1: + path: "${p.posix.prettyUri(workingDir.dir('plugin1').path)}" +'''); + }); + + test('supports out of date package_config.json', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + // Offline resolution will fail because "custom_lint_builder" is not + // present in the package_config.json naturally + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + final tempDir = createTemporaryDirectory(); + await workspace.resolvePluginHost(tempDir); + + final packageConfigJson = jsonDecode( + tempDir.packageConfig.readAsStringSync(), + ) as Map; + + expect(packageConfigJson['generator'], 'pub'); + }); + + test('queries pub.dev', () async { + final workingDir = + await createSimpleWorkspace(withPackageConfig: false, [ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + Pubspec( + 'b', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + // Same path but have it manually resolved differently in package_config, + // to have offline resolution fail. + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + final aPackageConfig = PackageConfig([ + Package( + 'custom_lint_builder', + workingDir.dir('custom_lint_builder').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + Package( + 'plugin1', + workingDir.dir('plugin1').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + Package( + 'a', + workingDir.dir('a').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + ]); + workingDir + .dir('a') // + .packageConfig + .writeAsStringSync( + jsonEncode(PackageConfig.toJson(aPackageConfig)), + ); + final bPackageConfig = PackageConfig([ + Package( + 'custom_lint_builder', + workingDir.dir('custom_lint_builder').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + Package( + 'plugin1', + workingDir.dir('plugin12').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + Package( + 'b', + workingDir.dir('b').uri, + languageVersion: LanguageVersion.parse('3.0'), + ), + ]); + workingDir + .dir('b') // + .packageConfig + .writeAsStringSync( + jsonEncode(PackageConfig.toJson(bPackageConfig)), + ); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + final tempDir = createTemporaryDirectory(); + await workspace.resolvePluginHost(tempDir); + + final packageConfigJson = jsonDecode( + tempDir.packageConfig.readAsStringSync(), + ) as Map; + + expect(packageConfigJson['generator'], 'pub'); + }); + }); + + group('runPubGet', () { + test('throws if pub get fails', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + await expectLater( + // Pub get will fail due to missing SDK constraints. + () => workspace.runPubGet(workingDir.dir('a')), + throwsA( + isA().having( + (e) => e.toString(), + 'toString', + contains('Failed to run "pub get" in the client project:\n'), + ), + ), + ); + }); + + test('resolves if pub get succeeds', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + final processes = spyProcess(); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(processes, isEmpty); + + await expectLater( + workspace.runPubGet(workingDir.dir('a')), + completes, + ); + + expect( + processes.removeFirst(), + ( + executable: Platform.resolvedExecutable, + args: const ['pub', 'get'], + runInShell: false, + ), + ); + expect(processes, isEmpty); + }); + + test('only spawns a shell when running in Windows', () async { + final workingDir = await createSimpleWorkspace([ + 'custom_lint_builder', + Pubspec( + 'plugin1', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.parse('^3.0.0')}, + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + final processes = spyProcess(); + + platformIsWindows = true; + await workspace.runPubGet(workingDir.dir('a')); + expect(processes.last.runInShell, true); + + platformIsWindows = false; + await workspace.runPubGet(workingDir.dir('a')); + expect(processes.last.runInShell, false); + }); + }); + + group('isUsingFlutter', () { + test('returns true if flutter is found in any project in the workspace', + () async { + final workingDir = await createSimpleWorkspace([ + 'flutter', + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency(), + 'flutter': SdkDependency('flutter'), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.isUsingFlutter, true); + }); + + test( + 'returns false if flutter is not found in any project in the workspace', + () async { + final workingDir = await createSimpleWorkspace([ + 'flutter', + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.isUsingFlutter, false); + }); + }); + + group('computePubspec', () { + test( + 'If an environment constraint is not specified in a given project, it is considered as "^3.0.0"', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: {'sdk': VersionConstraint.any}, + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + environment: {}, + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + plugin1: any +'''); + }); + + test('Specifies environment such that it is compatible with all packages', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: { + 'sdk': VersionConstraint.parse('>=3.12.0 <4.0.0'), + }, + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + environment: { + 'sdk': VersionConstraint.parse('>=3.0.0 <3.19.0'), + }, + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +environment: + sdk: ">=3.12.0 <3.19.0" + +dependencies: + plugin1: any +'''); + }); + + test('Throws if there is no compatible environment', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + environment: { + 'sdk': VersionConstraint.parse('>=2.12.0 <2.15.0'), + }, + devDependencies: {'plugin1': HostedDependency()}, + ), + Pubspec( + 'b', + environment: { + 'sdk': VersionConstraint.parse('>=2.16.0 <2.19.0'), + }, + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The environment "sdk" has incompatible version constraints in the project: +- ">=2.12.0 <2.15.0" + from "a" at "./a/pubspec.yaml". +- ">=2.16.0 <2.19.0" + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test( + 'if a package has for SDK >2<3 and another has >3<4, ' + 'they should be considered compatible', () { + // This is due to the SDK overriding <3 to <4 + }); + + test( + 'If a dependency is used with version numbers, ' + 'use a version range compatible with all packages', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('>=1.0.0 <1.5.0'), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.3.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: ">=1.3.0 <1.5.0" +'''); + }); + + test('Throws if no valid version range is found', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('>=2.12.0 <2.15.0'), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('>=2.16.0 <2.19.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- ">=2.12.0 <2.15.0" + from "a" at "./a/pubspec.yaml". +- ">=2.16.0 <2.19.0" + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test( + 'Version conflicts in dev_dependencies are ignored if a valid dependency_overrides is present.', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('>=2.12.0 <2.15.0'), + ), + }, + dependencyOverrides: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('>=2.16.0 <2.19.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: any + +dependency_overrides: + plugin1: "^1.0.0" +'''); + }); + + test( + 'dev_dependencies with a dependency_override are still listed, ' + 'but with an any version', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^0.0.0'), + ), + }, + dependencyOverrides: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: any + +dependency_overrides: + plugin1: "^1.0.0" +'''); + }); + + test( + 'If a workspace has no dev_dependencies, no "dev_dependencies" should not be present in the pubspec.yaml', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'a', + dependencyOverrides: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependency_overrides: + plugin1: "^1.0.0" +'''); + }); + + test( + 'If a workspace has no dependency_overrides, it should not be present in the pubspec.yaml', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: "^1.0.0" +'''); + }); + + test( + 'Throws if a package uses different dependency type (path vs version vs ...)', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': HostedDependency()}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- "../plugin1" + from "a" at "./a/pubspec.yaml". +- any + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('Throws if a dependency uses two different paths', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': PathDependency('../plugin12')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- "../plugin1" + from "a" at "./a/pubspec.yaml". +- "../plugin12" + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('supports sdk dependencies', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': SdkDependency('flutter')}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': SdkDependency('flutter')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec(), + ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + sdk: flutter +''', + ); + }); + + test('throws on incompatible sdk dependencies', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': SdkDependency('dart')}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': SdkDependency('flutter')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- sdk: dart + from "a" at "./a/pubspec.yaml". +- sdk: flutter + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('Supports two different paths if both resolve to the same directory', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: {'plugin1': PathDependency('../plugin1')}, + ), + Pubspec( + 'b', + devDependencies: {'plugin1': PathDependency('./../plugin1')}, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + path: "${p.posix.prettyUri(workingDir.dir('plugin1').path)}" +'''); + }); + + group('Supports git projects', () { + test('with no ref', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + git: + url: https://google.com +'''); + }); + + test('with no path', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + git: + url: https://google.com + ref: master +'''); + }); + + test('and all its parameters', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + git: + url: https://google.com + ref: master + path: "/packages/plugin1" +'''); + }); + }); + + group('Throws if hosted version dependencies', () { + test('have different declaredName', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + 'google', + Uri.parse('https://google.com'), + ), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + 'google2', + Uri.parse('https://google.com'), + ), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- any + from "a" at "./a/pubspec.yaml". +- any + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('have different host urls', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + 'https://google.com', + Uri.parse('https://google.com'), + ), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + 'https://google.com', + Uri.parse('https://google2.com'), + ), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- any + from "a" at "./a/pubspec.yaml". +- any + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + }); + + group('Throws if git dependencies', () { + test('have different url', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google2.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- git: https://google.com + from "a" at "./a/pubspec.yaml". +- git: https://google2.com + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('have different path', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin2', + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- git: https://google.com + from "a" at "./a/pubspec.yaml". +- git: https://google.com + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + + test('have different ref', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'master', + path: '/packages/plugin1', + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': GitDependency( + Uri.parse('https://google.com'), + ref: 'dev', + path: '/packages/plugin1', + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect( + workspace.computePubspec, + throwsA( + isA() + .having((e) => e.toString(), 'toString', ''' +The package "plugin1" has incompatible version constraints in the project: +- git: https://google.com + from "a" at "./a/pubspec.yaml". +- git: https://google.com + from "b" at "./b/pubspec.yaml". +'''), + ), + ); + }); + }); + + test( + 'The generated pubspec must contains only plugins and dependency_overrides', + () async { + final workingDir = await createSimpleWorkspace([ + 'dep', + 'override', + 'dev_dep', + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + dependencies: { + 'dep': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + 'dev_dep': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + dependencyOverrides: { + 'override': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: "^1.0.0" + +dependency_overrides: + override: "^1.0.0" +'''); + }); + + test( + 'if a dependency is used as "dev_dependencies" in all packages using it, ' + 'it stays a "dev_dependencies"', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'plugin2', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + 'plugin2': HostedDependency( + version: VersionConstraint.parse('^1.2.0'), + ), + }, + ), + Pubspec( + 'b', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: ">=1.0.0 <2.0.0" + plugin2: "^1.2.0" +'''); + }); + + test( + 'If a plugin is sometimes a dev_dependency and sometimes a dependency_overrides, ' + 'ignore constraints specified by dev_dependencies', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + Pubspec( + 'b', + dependencyOverrides: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^2.0.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: any + +dependency_overrides: + plugin1: "^2.0.0" +'''); + }); + + test( + 'If a plugin is a dev_dependency and a regular dependency, the constraint is the intersection of both', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.0.0'), + ), + }, + ), + Pubspec( + 'b', + dependencies: { + 'plugin1': HostedDependency( + version: VersionConstraint.parse('^1.5.0'), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a', 'b'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: ">=1.5.0 <2.0.0" +'''); + }); + group( + 'Support hosted project with custom source', + () { + test( + 'If a dependency comes from a custom hosted source, the generated pubspec.yaml should contain the hosted source', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: { + 'custom_lint_builder': HostedDependency(), + }, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + 'plugin1', + Uri.parse('https://custom.com'), + ), + version: Version(1, 0, 0), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + hosted: + name: plugin1 + url: https://custom.com + version: "1.0.0" +'''); + }); + test( + 'Hosted withouth name should still work', + () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + dependencies: { + 'custom_lint_builder': HostedDependency(), + }, + ), + Pubspec( + 'a', + devDependencies: { + 'plugin1': HostedDependency( + hosted: HostedDetails( + null, + Uri.parse('https://custom.com'), + ), + version: Version(1, 0, 0), + ), + }, + ), + ]); + + final workspace = await fromContextRootsFromPaths( + ['a'], + workingDirectory: workingDir, + ); + + expect(workspace.computePubspec(), ''' +name: custom_lint_client +description: A client for custom_lint +version: 0.0.1 +publish_to: 'none' + +dependencies: + plugin1: + hosted: + url: https://custom.com + version: "1.0.0" +'''); + }, + ); + }, + ); + }); + + group(CustomLintWorkspace.fromPaths, () { + test('decode CustomLintProject.pubspecOverrides', () async { + final workingDir = await createSimpleWorkspace([ + Pubspec( + 'plugin', + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'with_override', + devDependencies: {'plugin': HostedDependency()}, + ), + Pubspec( + 'no_override', + devDependencies: {'plugin': HostedDependency()}, + ), + ]); + + workingDir.dir('with_override').pubspecOverrides.writeAsStringSync(''' +dependency_overrides: + plugin: any +'''); + + final workspace = await fromContextRootsFromPaths( + ['with_override', 'no_override'], + workingDirectory: workingDir, + ); + + expect(workspace.projects, hasLength(2)); + + expect(workspace.projects.first.pubspec.name, 'with_override'); + expect(workspace.projects[1].pubspec.name, 'no_override'); + + expect( + workspace.projects.first.pubspecOverrides, + { + 'plugin': isA() + .having((e) => e.hosted, 'hosted', null) + .having((e) => e.version, 'version', VersionConstraint.any), + }, + ); + expect(workspace.projects[1].pubspecOverrides, isNull); + }); + + test('Handles relative paths', () async { + final workspace = await createSimpleWorkspace(['package']); + + writeFile( + workspace.dir('package').analysisOptions, + analysisOptionsWithCustomLintEnabled, + ); + + final customLintWorkspace = await CustomLintWorkspace.fromPaths( + ['package'], + workingDirectory: workspace, + ); + + expect(customLintWorkspace.contextRoots, hasLength(1)); + expect( + customLintWorkspace.contextRoots.first.root, + workspace.dir('package').path, + ); + + expect( + customLintWorkspace.projects.map((e) => e.pubspec.name), + ['package'], + ); + }); + + test('Decodes contextRoots', () async { + final workspace = await createSimpleWorkspace([ + 'package', + p.join('package', 'subpackage'), + ]); + + writeFile( + workspace.dir('package').analysisOptions, + analysisOptionsWithCustomLintEnabled, + ); + + final customLintWorkspace = await CustomLintWorkspace.fromPaths( + [workspace.path], + workingDirectory: workspace, + ); + + expect(customLintWorkspace.contextRoots, hasLength(2)); + + expect( + customLintWorkspace.contextRoots, + unorderedMatches([ + isA() + .having((e) => e.root, 'root', workspace.dir('package').path) + .having((e) => e.exclude, 'exclude', [ + workspace.dir('package', 'subpackage').path, + ]).having( + (e) => e.optionsFile, + 'optionsFile', + workspace.dir('package').analysisOptions.path, + ), + isA() + .having( + (e) => e.root, + 'root', + workspace.dir('package', 'subpackage').path, + ) + .having((e) => e.exclude, 'exclude', isEmpty) + .having( + (e) => e.optionsFile, + 'optionsFile', + workspace.dir('package').analysisOptions.path, + ), + ]), + ); + }); + + test( + 'When looking for projects with custom_lint enabled, ' + 'supports analysis_options.yaml imports', () async { + final workspace = await createSimpleWorkspace([ + 'enabled_import', + 'disabled_import', + ]); + final enabledAnalysisOptions = workspace.dir('enabled').analysisOptions; + writeFile(enabledAnalysisOptions, analysisOptionsWithCustomLintEnabled); + + final disabledAnalysisOptions = + workspace.dir('disabled').analysisOptions; + writeFile( + disabledAnalysisOptions, + analysisOptionsWithCustomLintDisabled, + ); + + final enabledImportOptions = + workspace.dir('enabled_import').analysisOptions; + writeFile( + enabledImportOptions, + 'include: ${enabledAnalysisOptions.path}', + ); + + final disabledImportOptions = + workspace.dir('disabled_import').analysisOptions; + writeFile( + disabledImportOptions, + 'include: ${disabledAnalysisOptions.path}', + ); + + final customLintWorkspace = await CustomLintWorkspace.fromPaths( + [workspace.path], + workingDirectory: workspace, + ); + + expect( + customLintWorkspace.projects.map((e) => e.pubspec.name), + ['enabled_import'], + ); + }); + + test('Parses all projects in the directory where custom_lint is enabled', + () async { + final workspace = await createSimpleWorkspace([ + // A folder with no analysis_options.yaml + 'disabled_package', + // A folder with an analysis_options.yaml that does not have custom_lint enabled + 'explicitly_disabled_package', + // A folder with an analysis_options.yaml that has custom_lint enabled + 'package_with_custom_lint', + ]); + + final subPackage = workspace.dir('disabled_package').dir('sub_package'); + await createProject( + subPackage, + Pubspec('sub_package', version: Version(1, 0, 0)), + packageConfigs: [], + ); + + // Enable custon_lint in sub_package and package_with_custom_lint + final analysisOptions = [ + workspace.dir('package_with_custom_lint').analysisOptions, + subPackage.analysisOptions, + // Insert the analysis_options.yaml above the project to test that + // the resolution handles inherited analysis_options.yaml files. + workspace.dir('packages').analysisOptions, + ]; + for (final file in analysisOptions) { + writeFile(file, analysisOptionsWithCustomLintEnabled); + } + + writeFile( + workspace.dir('explicitly_disabled_package').analysisOptions, + analysisOptionsWithCustomLintDisabled, + ); + + final customLintWorkspace = await CustomLintWorkspace.fromPaths( + [workspace.path], + workingDirectory: workspace, + ); + + expect( + customLintWorkspace.projects.map((e) => e.pubspec.name), + unorderedEquals([ + 'package_with_custom_lint', + 'sub_package', + ]), + ); + }); + }); + + group('fromContextRoots', () { + /// Shorthand for calling [CustomLintWorkspace.fromContextRoots] from + /// a list of path. + Future fromContextRootsFromPaths( + List paths, { + required Directory workingDirectory, + }) async { + return CustomLintWorkspace.fromContextRoots( + paths.map((path) => ContextRoot(path, [])).toList(), + workingDirectory: workingDirectory, + ); + } + + test( + 'finds pubspecs above analysis options file if there exists one', + () async { + final workspace = await createSimpleWorkspace(['package']); + + final analysisFile = workspace.dir('package').analysisOptions; + analysisFile.createSync(); + analysisFile.writeAsStringSync(analysisOptionsWithCustomLintEnabled); + final testDir = workspace.dir('package', 'test'); + testDir.packageConfig.createSync(recursive: true); + final nestedAnalysisFile = testDir.analysisOptions; + nestedAnalysisFile.createSync(recursive: true); + nestedAnalysisFile + .writeAsStringSync(analysisOptionsWithCustomLintEnabled); + + final customLintWorkspace = await CustomLintWorkspace.fromPaths( + [p.join(workspace.path, 'package')], + workingDirectory: workspace, + ); + // Expect one context root for the workspace and one for the test folder + expect(customLintWorkspace.contextRoots, hasLength(2)); + }, + ); + + test( + 'throws PackageConfigParseError if package has a pubspec but no .dart_tool/package_config.json', + () async { + final workspace = await createSimpleWorkspace(['package']); + workspace.dir('package', '.dart_tool').deleteSync(recursive: true); + + expect( + () => fromContextRootsFromPaths( + [p.join(workspace.path, 'package')], + workingDirectory: workspace, + ), + throwsA(isA()), + ); + }); + + test( + 'throws PackageConfigParseError if package has a malformed .dart_tool/package_config.json', + () async { + final workspace = await createSimpleWorkspace(['package']); + workspace + .dir('package', '.dart_tool') + .file('package_config.json') + .writeAsStringSync('malformed'); + + expect( + () => fromContextRootsFromPaths( + [p.join(workspace.path, 'package')], + workingDirectory: workspace, + ), + throwsA(isA()), + ); + }); + + test('throws PubspecParseError if package has a malformed pubspec.yaml', + () async { + final workspace = await createSimpleWorkspace(['package']); + workspace + .dir('package') + .file('pubspec.yaml') + .writeAsStringSync('malformed'); + + expect( + () => fromContextRootsFromPaths( + [p.join(workspace.path, 'package')], + workingDirectory: workspace, + ), + throwsA(isA()), + ); + }); + + test('Supports empty workspace', () async { + final customLintWorkspace = await fromContextRootsFromPaths( + [], + // The working directory is not used, but it is required by the API. + workingDirectory: Directory.current, + ); + + expect(customLintWorkspace.contextRoots, isEmpty); + expect(customLintWorkspace.uniquePluginNames, isEmpty); + expect(customLintWorkspace.projects, isEmpty); + }); + + test('Supports projects with no plugins', () async { + final workspace = await createSimpleWorkspace(['package']); + + final customLintWorkspace = await fromContextRootsFromPaths( + [p.join(workspace.path, 'package')], + workingDirectory: workspace, + ); + + expect( + customLintWorkspace.contextRoots.map((e) => e.root), + [p.join(workspace.path, 'package')], + ); + // No plugin is used, so the list of unique plugin names is empty. + expect(customLintWorkspace.uniquePluginNames, isEmpty); + + expect(customLintWorkspace.projects, hasLength(1)); + expect(customLintWorkspace.projects.first.plugins, isEmpty); + expect( + customLintWorkspace.projects.first.packageConfig.packages, + isEmpty, + ); + expect( + customLintWorkspace.projects.first.directory.path, + p.join(workspace.path, 'package'), + ); + expect(customLintWorkspace.projects.first.pubspec.name, 'package'); + }); + + test('Supports plugins in regular dependencies', () async { + final workspace = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + version: Version(1, 0, 0), + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + version: Version(1, 0, 0), + dependencies: { + 'plugin1': HostedDependency(), + }, + ), + ]); + final customLintWorkspace = await fromContextRootsFromPaths( + [p.join(workspace.path, 'a')], + workingDirectory: workspace, + ); + expect( + customLintWorkspace.uniquePluginNames, + {'plugin1'}, + ); + }); + + test('Supports plugins in regular and dev dependencies', () async { + final workspace = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + version: Version(1, 0, 0), + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'a', + version: Version(1, 0, 0), + dependencies: { + 'plugin1': HostedDependency(), + }, + devDependencies: { + 'plugin1': HostedDependency(), + }, + ), + ]); + final customLintWorkspace = await fromContextRootsFromPaths( + [p.join(workspace.path, 'a')], + workingDirectory: workspace, + ); + expect( + customLintWorkspace.uniquePluginNames, + {'plugin1'}, + ); + }); + + test('Supports projects with shared plugins', () async { + final workspace = await createSimpleWorkspace([ + Pubspec( + 'plugin1', + version: Version(1, 0, 0), + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'plugin2', + version: Version(1, 0, 0), + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + Pubspec( + 'plugin3', + version: Version(1, 0, 0), + dependencies: {'custom_lint_builder': HostedDependency()}, + ), + 'random_dep', + 'custom_lint_builder', + Pubspec( + 'a', + version: Version(1, 0, 0), + devDependencies: { + 'plugin1': HostedDependency(), + 'plugin3': HostedDependency(), + 'random_dep': HostedDependency(), + }, + ), + Pubspec( + 'b', + version: Version(1, 0, 0), + devDependencies: { + 'plugin2': HostedDependency(), + 'plugin3': HostedDependency(), + 'random_dep': HostedDependency(), + }, + ), + ]); + + final customLintWorkspace = await fromContextRootsFromPaths( + [ + p.join(workspace.path, 'a'), + p.join(workspace.path, 'b'), + ], + workingDirectory: workspace, + ); + + expect( + customLintWorkspace.contextRoots.map((e) => e.root), + [p.join(workspace.path, 'a'), p.join(workspace.path, 'b')], + ); + // No plugin is used, so the list of unique plugin names is empty. + expect( + customLintWorkspace.uniquePluginNames, + {'plugin1', 'plugin2', 'plugin3'}, + ); + + expect(customLintWorkspace.projects, hasLength(2)); + + expect(customLintWorkspace.projects.first.plugins, hasLength(2)); + expect(customLintWorkspace.projects.first.plugins[0].name, 'plugin1'); + expect( + customLintWorkspace.projects.first.plugins[0].constraint, + isA(), + ); + expect(customLintWorkspace.projects.first.plugins[1].name, 'plugin3'); + expect( + customLintWorkspace.projects.first.plugins[1].constraint, + isA(), + ); + expect(customLintWorkspace.projects.first.pubspec.name, 'a'); + + expect(customLintWorkspace.projects[1].plugins, hasLength(2)); + expect(customLintWorkspace.projects[1].plugins[0].name, 'plugin2'); + expect( + customLintWorkspace.projects[1].plugins[1].constraint, + isA(), + ); + expect(customLintWorkspace.projects[1].plugins[1].name, 'plugin3'); + expect( + customLintWorkspace.projects[1].plugins[1].constraint, + isA(), + ); + expect(customLintWorkspace.projects[1].pubspec.name, 'b'); + }); + }); + }); + + group(PubspecDependency, () { + group('fromHostedDependency', () { + test('intersect', () { + final beforeV2 = PubspecDependency.fromDependency( + HostedDependency(version: VersionConstraint.parse('<2.0.0')), + ); + final afterV1 = PubspecDependency.fromDependency( + HostedDependency(version: VersionConstraint.parse('>=1.0.0')), + ); + + expect( + afterV1.intersect( + PubspecDependency.fromDependency( + HostedDependency(version: Version(0, 0, 1)), + ), + ), + null, + ); + + final intersection = afterV1.intersect(beforeV2)!; + final intersection2 = beforeV2.intersect(afterV1)!; + + for (final intersection in [intersection, intersection2]) { + expect( + intersection.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(1, 0, 0)), + ), + ), + true, + ); + expect( + intersection.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(1, 1, 0)), + ), + ), + true, + ); + expect( + intersection.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(2, 0, 0)), + ), + ), + false, + ); + expect( + intersection.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(0, 1, 0)), + ), + ), + false, + ); + } + }); + + test('isCompatibleWith', () { + final from10 = PubspecDependency.fromDependency( + HostedDependency(version: VersionConstraint.parse('^1.0.0')), + ); + final from11 = PubspecDependency.fromDependency( + HostedDependency(version: VersionConstraint.parse('^1.1.0')), + ); + + expect(from10.isCompatibleWith(from10), true); + expect(from10.isCompatibleWith(from11), true); + expect(from11.isCompatibleWith(from10), true); + expect( + from10.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(1, 0, 0)), + ), + ), + true, + ); + expect( + from10.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency(version: Version(2, 0, 0)), + ), + ), + false, + ); + + final hosted = PubspecDependency.fromDependency( + HostedDependency( + version: Version(1, 0, 0), + hosted: HostedDetails('name', Uri.parse('google.com')), + ), + ); + expect( + hosted.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency( + version: Version(1, 0, 0), + hosted: HostedDetails('name', Uri.parse('google.com')), + ), + ), + ), + true, + ); + expect( + hosted.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency( + version: Version(1, 0, 0), + hosted: HostedDetails('name2', Uri.parse('google.com')), + ), + ), + ), + false, + ); + expect( + hosted.isCompatibleWith( + PubspecDependency.fromDependency( + HostedDependency( + version: Version(1, 0, 0), + hosted: HostedDetails('name', Uri.parse('google2.com')), + ), + ), + ), + false, + ); + }); + }); + + group('fromSdkDependency', () { + test('intersect', () { + final dependency = PubspecDependency.fromDependency( + SdkDependency('path'), + ); + + expect( + dependency.intersect( + PubspecDependency.fromDependency( + SdkDependency('path'), + ), + ), + same(dependency), + ); + expect( + dependency.intersect( + PubspecDependency.fromDependency( + SdkDependency('path2'), + ), + ), + null, + ); + }); + + test('isCompatibleWith', () { + final dependency = PubspecDependency.fromDependency( + SdkDependency('path'), + ); + + expect(dependency.isCompatibleWith(dependency), true); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + SdkDependency('path'), + ), + ), + true, + ); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + SdkDependency('path2'), + ), + ), + false, + ); + }); + }); + + group('fromPathDependency', () { + test('intersect', () { + final dependency = PubspecDependency.fromDependency( + PathDependency('path'), + ); + + expect( + dependency.intersect( + PubspecDependency.fromDependency( + PathDependency('path'), + ), + ), + same(dependency), + ); + expect( + dependency.intersect( + PubspecDependency.fromDependency( + PathDependency('path2'), + ), + ), + null, + ); + }); + + test('isCompatibleWith', () { + final dependency = PubspecDependency.fromDependency( + PathDependency('path'), + ); + + expect(dependency.isCompatibleWith(dependency), true); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + PathDependency('path'), + ), + ), + true, + ); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + PathDependency('path2'), + ), + ), + false, + ); + }); + }); + + group('fromGitDependency', () { + test('intersect', () { + final dependency = PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path', + ref: '01', + ), + ); + + expect( + dependency.intersect( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path', + ref: '01', + ), + ), + ), + same(dependency), + ); + expect( + dependency.intersect( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com2'), + path: 'path', + ref: '01', + ), + ), + ), + null, + ); + }); + + test('isCompatibleWith', () { + final dependency = PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path', + ref: '01', + ), + ); + + expect(dependency.isCompatibleWith(dependency), true); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path', + ref: '01', + ), + ), + ), + true, + ); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google2.com'), + path: 'path', + ref: '01', + ), + ), + ), + false, + ); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path2', + ref: '01', + ), + ), + ), + false, + ); + expect( + dependency.isCompatibleWith( + PubspecDependency.fromDependency( + GitDependency( + Uri.parse('google.com'), + path: 'path', + ref: '013', + ), + ), + ), + false, + ); + }); + }); + }); +} diff --git a/packages/custom_lint/tools/analyzer_plugin/.dart_tool/package_config.json b/packages/custom_lint/tools/analyzer_plugin/.dart_tool/package_config.json deleted file mode 100644 index 049f5eb3..00000000 --- a/packages/custom_lint/tools/analyzer_plugin/.dart_tool/package_config.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "_fe_analyzer_shared", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "analyzer", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "analyzer_plugin", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "args", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "async", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "charcode", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "collection", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "convert", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "crypto", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint", - "rootUri": "file:///Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "dart_style", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "file", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "glob", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "meta", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "package_config", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "path", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pub_semver", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "source_span", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "string_scanner", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "term_glyph", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "typed_data", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "watcher", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint_analyzer_plugin_loader", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "2.14" - } - ], - "generated": "2022-03-03T11:16:55.205661Z", - "generator": "pub", - "generatorVersion": "2.16.1" -} diff --git a/packages/custom_lint/tools/analyzer_plugin/.packages b/packages/custom_lint/tools/analyzer_plugin/.packages deleted file mode 100644 index f3ecb19f..00000000 --- a/packages/custom_lint/tools/analyzer_plugin/.packages +++ /dev/null @@ -1,30 +0,0 @@ -# This file is deprecated. Tools should instead consume -# `.dart_tool/package_config.json`. -# -# For more info see: https://dart.dev/go/dot-packages-deprecation -# -# Generated by pub on 2022-03-03 12:16:55.180957. -_fe_analyzer_shared:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0/lib/ -analyzer:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1/lib/ -analyzer_plugin:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0/lib/ -args:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ -async:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ -charcode:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ -collection:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/ -convert:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ -crypto:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ -custom_lint:file:///Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/lib/ -dart_style:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.1/lib/ -file:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ -glob:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ -meta:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -package_config:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ -path:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ -pub_semver:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.0/lib/ -source_span:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ -string_scanner:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ -term_glyph:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ -typed_data:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ -watcher:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ -yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ -custom_lint_analyzer_plugin_loader:lib/ diff --git a/packages/custom_lint/tools/analyzer_plugin/bin/log.dart b/packages/custom_lint/tools/analyzer_plugin/bin/log.dart deleted file mode 100644 index 32644a32..00000000 --- a/packages/custom_lint/tools/analyzer_plugin/bin/log.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'dart:io'; - -@deprecated -final file = File( - '/Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/log.txt', -); - -void log(Object obj) { - file.writeAsStringSync( - '\n${DateTime.now()} $obj', - mode: FileMode.append, - ); -} diff --git a/packages/custom_lint/tools/analyzer_plugin/bin/plugin.dart b/packages/custom_lint/tools/analyzer_plugin/bin/plugin.dart index 1bb57020..1fe26329 100644 --- a/packages/custom_lint/tools/analyzer_plugin/bin/plugin.dart +++ b/packages/custom_lint/tools/analyzer_plugin/bin/plugin.dart @@ -1,11 +1,7 @@ import 'dart:isolate'; -import 'package:custom_lint/src/analyzer_plugin/analyzer_plugin_starter.dart'; +import 'package:custom_lint/src/analyzer_plugin_starter.dart'; -import 'log.dart'; - -void main(List args, SendPort sendPort) { - log('main'); - start(args, sendPort); - // Future.delayed(Duration(hours: 2)); +Future main(List args, SendPort sendPort) async { + await start(args, sendPort); } diff --git a/packages/custom_lint/tools/analyzer_plugin/pubspec.lock b/packages/custom_lint/tools/analyzer_plugin/pubspec.lock deleted file mode 100644 index d555083a..00000000 --- a/packages/custom_lint/tools/analyzer_plugin/pubspec.lock +++ /dev/null @@ -1,166 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "36.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.0" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - custom_lint: - dependency: "direct main" - description: - path: "/Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/" - relative: false - source: path - version: "0.0.1" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.1" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" -sdks: - dart: ">=2.16.0 <3.0.0" diff --git a/packages/custom_lint/tools/analyzer_plugin/pubspec.yaml b/packages/custom_lint/tools/analyzer_plugin/pubspec.yaml index 30ca65b1..f9b52ed2 100644 --- a/packages/custom_lint/tools/analyzer_plugin/pubspec.yaml +++ b/packages/custom_lint/tools/analyzer_plugin/pubspec.yaml @@ -1,11 +1,16 @@ name: custom_lint_analyzer_plugin_loader description: This pubspec determines the version of the analyzer plugin to load. -version: 4.11.0 +version: 0.7.0 publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - custom_lint: - path: /Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/ + custom_lint: 0.7.5 + +# TODO: If you want to contribute to custom_lint, add a pubspec_overrides.yaml file +# in this folder, containing the following: +# dependency_overrides: +# custom_lint: +# path: /absolute/path/to/custom_lint/folder diff --git a/packages/custom_lint_builder/.dart_tool/package_config.json b/packages/custom_lint_builder/.dart_tool/package_config.json deleted file mode 100644 index 3e981658..00000000 --- a/packages/custom_lint_builder/.dart_tool/package_config.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "_fe_analyzer_shared", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "analyzer", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "analyzer_plugin", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "args", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "async", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "charcode", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "checked_yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "collection", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "convert", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "crypto", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint", - "rootUri": "../../custom_lint", - "packageUri": "lib/", - "languageVersion": "2.16" - }, - { - "name": "dart_style", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "file", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "glob", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "json_annotation", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "meta", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "package_config", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "path", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pub_semver", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "pubspec_parse", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "recase", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "source_span", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "string_scanner", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "term_glyph", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "typed_data", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "uuid", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "watcher", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "yaml", - "rootUri": "file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "custom_lint_builder", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "2.16" - } - ], - "generated": "2022-03-15T15:30:53.832563Z", - "generator": "pub", - "generatorVersion": "2.16.1" -} diff --git a/packages/custom_lint_builder/.packages b/packages/custom_lint_builder/.packages deleted file mode 100644 index 486027df..00000000 --- a/packages/custom_lint_builder/.packages +++ /dev/null @@ -1,35 +0,0 @@ -# This file is deprecated. Tools should instead consume -# `.dart_tool/package_config.json`. -# -# For more info see: https://dart.dev/go/dot-packages-deprecation -# -# Generated by pub on 2022-03-15 16:30:53.816051. -_fe_analyzer_shared:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-36.0.0/lib/ -analyzer:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer-3.3.1/lib/ -analyzer_plugin:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/analyzer_plugin-0.9.0/lib/ -args:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ -async:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ -charcode:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ -checked_yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/checked_yaml-2.0.1/lib/ -collection:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0/lib/ -convert:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ -crypto:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ -custom_lint:../custom_lint/lib/ -dart_style:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/dart_style-2.2.2/lib/ -file:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ -glob:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ -json_annotation:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/json_annotation-4.4.0/lib/ -meta:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ -package_config:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ -path:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ -pub_semver:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/ -pubspec_parse:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/pubspec_parse-1.2.0/lib/ -recase:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/recase-4.0.0/lib/ -source_span:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ -string_scanner:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ -term_glyph:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ -typed_data:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ -uuid:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/uuid-3.0.6/lib/ -watcher:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ -yaml:file:///Users/remirousselet/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ -custom_lint_builder:lib/ diff --git a/packages/custom_lint_builder/CHANGELOG.md b/packages/custom_lint_builder/CHANGELOG.md new file mode 100644 index 00000000..2a4479ac --- /dev/null +++ b/packages/custom_lint_builder/CHANGELOG.md @@ -0,0 +1,412 @@ +## 0.7.5 - 2025-02-27 + +Fix inconsistent version + +## 0.7.4 - 2025-02-27 + +- `custom_lint` upgraded to `0.7.4` + +## 0.7.3 - 2025-02-08 + +- `custom_lint` upgraded to `0.7.3` + +## 0.7.2 - 2025-01-29 + +- `custom_lint` upgraded to `0.7.2` + +## 0.7.1 - 2025-01-08 + +- Support analyzer 7.0.0 + +## 0.7.0 - 2024-10-27 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + +## 0.6.10 - 2024-10-10 + +- `custom_lint` upgraded to `0.6.10` +- `custom_lint_core` upgraded to `0.6.10` + +## 0.6.9 - 2024-10-09 + +- `custom_lint` upgraded to `0.6.9` +- `custom_lint_core` upgraded to `0.6.9` + +## 0.6.8 - 2024-10-08 + +- Fix CI + +## 0.6.7 - 2024-09-08 + +- `custom_lint` upgraded to `0.6.7` + +## 0.6.6 - 2024-09-08 + +- `custom_lint` upgraded to `0.6.6` + +## 0.6.5 - 2024-08-15 + +- Upgraded to analyzer ^6.6.0. + This is a quick fix to unblock the stable Flutter channel. + A more robust fix will come later. +- Fixed a bug where isSuperTypeOf throws if the element is null (thanks to @charlescyt) + +## 0.6.4 - 2024-03-16 + +- `custom_lint` upgraded to `0.6.4` + +## 0.6.3 - 2024-03-16 + +- `custom_lint` upgraded to `0.6.3` +- `custom_lint_core` upgraded to `0.6.3` + +## 0.6.2 - 2024-02-19 + +- Upgrade analyzer to support 6.4.0 +- Fix null exception when using `TypeChecker.isSuperTypeOf` (thanks to @charlescyt) + +## 0.6.1 - 2024-02-14 + +- `custom_lint_core` upgraded to `0.6.1` + +## 0.6.0 - 2024-02-04 + +- Bumped minimum Dart SDK to 3.0.0 +- Added support for `--fix` + +## 0.5.14 - 2024-02-03 + +- `custom_lint_core` upgraded to `0.5.14` + +## 0.5.13 - 2024-02-03 + +- `custom_lint_core` upgraded to `0.5.13` + +## 0.5.12 - 2024-02-02 + +- `custom_lint_core` upgraded to `0.5.12` + +## 0.5.11 - 2024-01-27 + +- Added support for `analysis_options.yaml` that are nt at the root of the project (thanks to @mrgnhnt96) + +## 0.5.10 - 2024-01-26 + +- `custom_lint_core` upgraded to `0.5.10` + +## 0.5.9 - 2024-01-26 + +- `custom_lint_core` upgraded to `0.5.9` + +## 0.5.8 - 2024-01-09 + +- `// ignore` comments now correctly respect indentation when they are inserted (thanks to @PiotrRogulski) + +## 0.5.7 - 2023-11-20 + +- `custom_lint` upgraded to `0.5.7` +- `custom_lint_core` upgraded to `0.5.7` + +## 0.5.6 - 2023-10-30 + +- `custom_lint` upgraded to `0.5.6` +- `custom_lint_core` upgraded to `0.5.6` + +## 0.5.5 - 2023-10-26 + +- `custom_lint` upgraded to `0.5.5` +- `custom_lint_core` upgraded to `0.5.5` + +## 0.5.4 - 2023-10-20 + +- `custom_lint` upgraded to `0.5.4` +- `custom_lint_core` upgraded to `0.5.4` + +## 0.5.3 - 2023-08-29 + +- `custom_lint` upgraded to `0.5.3` +- `custom_lint_core` upgraded to `0.5.3` + +## 0.5.2 - 2023-08-16 + +- Support both analyzer 5.12.0 and 6.0.0 at the same time. +- Attempt at fixing the windows crash + +## 0.5.0 - 2023-06-21 + +- `custom_lint` upgraded to `0.5.0` +- `custom_lint_core` upgraded to `0.5.0` + +## 0.4.0 - 2023-05-12 + +- Report uncaught exceptions inside `context.addPostRunCallback` +- Added support for analyzer 5.12.0 + +## 0.3.4 - 2023-04-19 + +- `custom_lint` upgraded to `0.3.4` +- `custom_lint_core` upgraded to `0.3.4` + +## 0.3.3 - 2023-04-06 + +- Upgraded `analyzer` to `>=5.7.0 <5.11.0` +- `LintRuleNodeRegistry` and other AstVisitor-like now are based off `GeneralizingAstVisitor` instead of `GeneralizingAstVisitor` +- Exposes the Pubspec in CustomLintContext + +## 0.3.2 - 2023-03-09 + +- `custom_lint` upgraded to `0.3.2` +- `custom_lint_core` upgraded to `0.3.2` + +## 0.3.1 - 2023-03-09 + +- `custom_lint_core` upgraded to `0.3.1` + +## 0.3.0 - 2023-03-09 + +- Fix FileSystemException thrown when deleting a file +- Update analyzer to >=5.7.0 <5.8.0 +- Upgrade dependencies + +## 0.2.12 + +Upgrade custom_lint + +## 0.2.11 + +- Fixes `LintCode.url` no-longer showing-up in the IDE +- Fix quick-fixes not working on the last offset of an analysis error +- Bump minimum Dart SDK to `sdk: ">=2.19.0 <3.0.0"` + +## 0.2.10 + +Fix `filesToAnalyze` only working on the file name instead of the file path. + +## 0.2.9 + +Fix `TypeChecker.fromPackage` not always return `true` when it should + +## 0.2.8 + +Fix exception thrown by `TypeChecker.isExactlyType` if `DartType.element` is `null`. + +## 0.2.7 + +Extract `LintRule` and similar other utilities to a separate package: `custom_lint_core`. +`custom_lint_builder` re-exports `custom_lint_core`, so the utilities are still available. + +## 0.2.6 + +Fix infinite loop on InconsistentAnalysisException + +## 0.2.5 + +- Fix custom_lint not correctly killing sub-processes when the IDE stops custom_lint. +- Export incorrectly unexported `matcherNormalizedPrioritizedSourceChangeSnapshot` + +## 0.2.4 + +- Added `.testRun` & `testAnalyzeAndRun` methods, which + enables programmatically running a lint/assist/fix against a Dart file. +- Added `matcherNormalizedPrioritizedSourceChangeSnapshot` test matcher, which + allows checking that a list of file edits matches against a JSON snapshot of the changes. + +## 0.2.3 + +Fixes InconsistentAnalysisException + +## 0.2.2 + +Fixes an exception thrown when a project contains images. + +## 0.2.1 + +Add `TypeChecker.every` and `TypeChecker.package` + +## 0.2.0 + +**Large Breaking change** +This new version introduces large changes to how lints/fixes/assists are defined. +Long story short, besides the `createPlugin` method, the entire syntax changed. + +See the readme, examples, and docs around how to use the new syntax. + +The new syntax has multiple benefits: + +- It is now possible to enable/disable lints inside the `analysis_options.yaml` + as followed: + + ```yaml + # optional + include: path/to/another/analysis_options.yaml + + custom_lint: + rules: + # enable a lint rule + - my_lint_rule + # A lint rule that is explicitly disabled + - another_lint_rule: false + ``` + + Enabling/disabling lints is supported by default with the new syntax. Nothing to do~ + +- Performance improvement when using a large number of lints. + The workload of analyzing files is now shared between lints. + +- The new syntax makes the code simpler to maintain. + Before, the `PluginBase.getLints` rapidly ended-up doing too much. + Now, it is simple to split the implementation in multiple bits + +## 0.1.2-dev + +Do some internal refactoring as an attempt to fix #60 + +## 0.1.1 + +- Fix an issue where plugins were hot-reloaded when the file analyzed changed. +- Optimized analysis such that `PluginBase.getLints()` is theorically not reinvoked + unless the file analyzed changed. + +## 0.1.0 + +- **Breaking**: The plugin entrypoint has moved. + Plugins no-longer should define a `/bin/custom_lint.dart` file. + Instead they should define a `/lib/.dart` + +- **Breaking**: The plugin entrypoint is modified. Plugins no-longer + define a "main", but instead define a `createPlugin` function: + + Before: + + ```dart + // /bin/custom_lint.dart + void main(List args, SendPort sendPort) { + startPlugin(sendPort, MyPlugin()); + } + ``` + + After: + + ```dart + // /lib/ MyPlugin(); + ``` + +- Add assist support. + Inside your plugins, you can now override `handleGetAssists`: + + ```dart + import 'package:analyzer_plugin/protocol/protocol_generated.dart' + as analyzer_plugin; + + class MyPlugin extends PluginBase { + // ... + + Future handleGetAssists( + ResolvedUnitResult resolvedUnitResult, { + required int offset, + required int length, + }) async { + // TODO return some assists for the given offset + } + } + ``` + +## 0.0.16 + +Fix `expect_lint` not working if the file doesn't contain any lint. + +## 0.0.15 + +- Custom_lint now has a built-in mechanism for testing lints. + Simply write a file that should contain lints for your plugin. + Then, using a syntax similar to `// ignore`, write a `// expect_lint: code` + in the line before your lint: + + ```dart + // expect_lint: riverpod_final_provider + var provider = Provider(...); + ``` + + When doing this, there are two possible cases: + + - The line after the `expect_lint` correctly contains the expected lint. + In that case, the lint is ignored (similarly to if we used `// ignore`) + - The next line does **not** contain the lint. + In that case, the `expect_lint` comment will have an error. + + This allows testing your plugins by simply running `custom_lint` on your test/example folder. + Then, if any expected lint is missing, the command will fail. But if your plugin correctly + emits the lint, the command will succeed. + +- Upgrade analyzer/analzer_plugin + +## 0.0.14 + +- Fix custom_lint not working in the IDE + +## 0.0.13 + +- Add debugger and hot-reload support (Thanks to @TimWhiting) +- Correctly respect `exclude` obtains from the analysis_options.yaml +- Fix `dart analyze` incorrectly failing due to showing the "plugin is starting" lint. + +## 0.0.12 + +Upgrade dependencies + +## 0.0.11 + +Upgrade dependencies + +## 0.0.10 + +- Upgrade Riverpod to 2.0.0 +- Fix deprecation errors with analyzer + +## 0.0.9 + +- Lint fixes can now be used when placing the cursor on the last character of a lint +- improve pub score + +## 0.0.8 + +Allow lints to emit fixes + +## 0.0.7 + +Fix a bug where the custom_lint command line may not list all lints + +## 0.0.6 + +feat!: getLints now is expected to return a `Stream` instead of `Iterable` + +fix: a bug where the lints shown by the IDE could get out of sync with the actual content of the file + +## 0.0.4 + +- Fixed a bug where the command line could show IDE-only meant for debugging + +## 0.0.3 + +PluginBase.getLints now receive a `ResolvedUnitResult` instead of a `LibraryElement`. + +## 0.0.2 + +- Compilation errors are now visible within the `pubspec.yaml` of applications + that are using the plugin. + +- Plugins that are currently loading are now highlighted inside the `pubspec.yaml` + of applications that are using the plugin. + +- If a plugin throws when trying to analyze a Dart file, the IDE will now + show the exception at the top of the analyzed file. + +- Compilation errors, exceptions and prints are now accessible within + a log file (`custom_lint.log`) inside applications using the plugin. + +## 0.0.1 + +Initial release diff --git a/packages/custom_lint_builder/LICENSE b/packages/custom_lint_builder/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/packages/custom_lint_builder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/custom_lint_builder/README.md b/packages/custom_lint_builder/README.md new file mode 100644 index 00000000..9b129990 --- /dev/null +++ b/packages/custom_lint_builder/README.md @@ -0,0 +1,37 @@ +

+

custom_lint_builder

+ An package for defining custom lints. +

+ +

+ License +

+ +## About + +`custom_lint_builder` is a package that should be associated with [custom_lint] +for defining custom_lint plugins. + + +If a package wants to access classes such as `LintRule` or `Assist` but do +not want to make a custom_lint plugin (such as for exposing new utilities +for plugin authors), then use `custom_lint_core` instead. + +Using `custom_lint_builder` is reserved to plugin authors. Depending it on it +will tell custom_lint that your package is a plugin, and therefore will try to +run it. + +See [custom_lint] for more informations + +--- + +

+ + + +

+ Built and maintained by Invertase. +

+

+ +[custom_lint]: https://github.com/invertase/dart_custom_lint diff --git a/packages/custom_lint_builder/analysis_options.yaml b/packages/custom_lint_builder/analysis_options.yaml deleted file mode 100644 index 4ef3092e..00000000 --- a/packages/custom_lint_builder/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: ../../analysis_options.yaml -analyzer: - plugins: - - custom_lint \ No newline at end of file diff --git a/packages/custom_lint_builder/example/README.md b/packages/custom_lint_builder/example/README.md new file mode 100644 index 00000000..075a67c5 --- /dev/null +++ b/packages/custom_lint_builder/example/README.md @@ -0,0 +1,3 @@ +# Custom Lint Example + +A simple example how powerful is custom_lint package. diff --git a/packages/custom_lint_builder/example/analysis_options.yaml b/packages/custom_lint_builder/example/analysis_options.yaml new file mode 100644 index 00000000..c63a09b8 --- /dev/null +++ b/packages/custom_lint_builder/example/analysis_options.yaml @@ -0,0 +1,19 @@ +include: ../../../analysis_options.yaml + +analyzer: + plugins: + - custom_lint + +linter: + rules: + public_member_api_docs: false + avoid_print: false + unreachable_from_main: false + +custom_lint: + rules: + - prefer_lint + - map: false + # - riverpod_final_provider: false + - map2: + length: 42 diff --git a/packages/custom_lint_builder/example/example.md b/packages/custom_lint_builder/example/example.md new file mode 100644 index 00000000..075a67c5 --- /dev/null +++ b/packages/custom_lint_builder/example/example.md @@ -0,0 +1,3 @@ +# Custom Lint Example + +A simple example how powerful is custom_lint package. diff --git a/packages/custom_lint_builder/example/example_lint/analysis_options.yaml b/packages/custom_lint_builder/example/example_lint/analysis_options.yaml new file mode 100644 index 00000000..5a2532a3 --- /dev/null +++ b/packages/custom_lint_builder/example/example_lint/analysis_options.yaml @@ -0,0 +1,6 @@ +# include: ../analysis_options.yaml + +# linter: +# rules: +# public_member_api_docs: false +# avoid_print: false diff --git a/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart b/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart new file mode 100644 index 00000000..6e47ae81 --- /dev/null +++ b/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart @@ -0,0 +1,183 @@ +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// This object is a utility for checking whether a Dart variable is assignable +/// to a given class. +/// +/// In this example, the class checked is `ProviderBase` from `package:riverpod`. +const _providerBaseChecker = TypeChecker.fromName( + 'ProviderBase', + packageName: 'riverpod', +); + +/// This is the entrypoint of our plugin. +/// All plugins must specify a `createPlugin` function in their `lib/.dart` file +PluginBase createPlugin() => _RiverpodLint(); + +/// The class listing all the [LintRule]s and [Assist]s defined by our plugin. +class _RiverpodLint extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + PreferFinalProviders(), + ]; + + @override + List getAssists() => [_ConvertToStreamProvider()]; +} + +/// A custom lint rule. +/// In our case, we want a lint rule which analyzes a Dart file. Therefore we +/// subclass [DartLintRule]. +/// +/// For emitting lints on non-Dart files, subclass [LintRule]. +class PreferFinalProviders extends DartLintRule { + PreferFinalProviders() : super(code: _code); + + /// Metadata about the lint define. This is the code which will show-up in the IDE, + /// and its description.. + static const _code = LintCode( + name: 'riverpod_final_provider', + problemMessage: 'Providers should be declared using the `final` keyword.', + ); + + /// The core logic for our custom lint rule. + /// In our case, it will search over all variables defined in a Dart file and + /// search for the ones that implement a specific type (see [_providerBaseChecker]). + @override + void run( + // This object contains metadata about the analyzed file + CustomLintResolver resolver, + // ErrorReporter is for submitting lints. It contains utilities to specify + // where the lint should show-up. + ErrorReporter reporter, + // This contains various utilities, including tools for inspecting the content + // of Dart files in an efficient manner. + CustomLintContext context, + ) { + // Using this function, we search for [VariableDeclaration] reference the + // analyzed Dart file. + context.registry.addVariableDeclaration((node) { + final element = node.declaredElement; + if (element == null || + element.isFinal || + // We check that the variable is a Riverpod provider + !_providerBaseChecker.isAssignableFromType(element.type)) { + return; + } + + // This emits our lint warning at the location of the variable. + reporter.atElement(element, code); + }); + } + + /// [LintRule]s can optionally specify a list of quick-fixes. + /// + /// Fixes will show-up in the IDE when the cursor is above the warning. And it + /// should contain a message explaining how the warning will be fixed. + @override + List getFixes() => [_MakeProviderFinalFix()]; +} + +/// We define a quick fix for an issue. +/// +/// Our quick fix wants to analyze Dart files, so we subclass [DartFix]. +/// For quick-fixes on non-Dart files, see [Fix]. +class _MakeProviderFinalFix extends DartFix { + /// Similarly to [LintRule.run], [Fix.run] is the core logic of a fix. + /// It will take care or proposing edits within a file. + @override + void run( + CustomLintResolver resolver, + // Similar to ErrorReporter, ChangeReporter is an object used for submitting + // edits within a Dart file. + ChangeReporter reporter, + CustomLintContext context, + // This is the warning that was emitted by our [LintRule] and which we are + // trying to fix. + AnalysisError analysisError, + // This is the other warnings in the same file defined by our [LintRule]. + // Useful in case we want to offer a "fix all" option. + List others, + ) { + // Using similar logic as in "PreferFinalProviders", we inspect the Dart file + // to search for variable declarations. + context.registry.addVariableDeclarationList((node) { + // We verify that the variable declaration is where our warning is located + if (!analysisError.sourceRange.intersects(node.sourceRange)) return; + + // We define one edit, giving it a message which will show-up in the IDE. + final changeBuilder = reporter.createChangeBuilder( + message: 'Make provider final', + // This represents how high-low should this qick-fix show-up in the list + // of quick-fixes. + priority: 1, + ); + + // Our edit will consist of editing a Dart file, so we invoke "addDartFileEdit". + // The changeBuilder variable also has utilities for other types of files. + changeBuilder.addDartFileEdit((builder) { + final nodeKeyword = node.keyword; + final nodeType = node.type; + if (nodeKeyword != null) { + // Replace "var x = ..." into "final x = ..."" + + // Using "builder", we can emit changes to a file. + // In this case, addSimpleReplacement is used to overrite a selection + // with a new content. + builder.addSimpleReplacement( + SourceRange(nodeKeyword.offset, nodeKeyword.length), + 'final', + ); + } else if (nodeType != null) { + // Replace "Type x = ..." into "final Type x = ..." + + // Once again we emit an edit to our file. + // But this time, we add new content without replacing existing content. + builder.addSimpleInsertion(nodeType.offset, 'final '); + } + }); + }); + } +} + +/// Using the same principle as we've seen before, we can define an "assist". +/// +/// The main difference between an [Assist] and a [Fix] is that a [Fix] is associated +/// with a problem. While an [Assist] is a change without an associated problem. +/// +/// These are commonly known as "refactoring". +class _ConvertToStreamProvider extends DartAssist { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + context.registry.addVariableDeclaration((node) { + // Check that the visited node is under the cursor + if (!target.intersects(node.sourceRange)) return; + + // verify that the visited node is a provider, to only show the assist on providers + final element = node.declaredElement; + if (element == null || + element.isFinal || + !_providerBaseChecker.isAssignableFromType(element.type)) { + return; + } + + final changeBuilder = reporter.createChangeBuilder( + priority: 1, + message: 'Convert to StreamProvider', + ); + changeBuilder.addDartFileEdit((builder) { + // + }); + }); + } +} diff --git a/packages/custom_lint_builder/example/example_lint/pubspec.yaml b/packages/custom_lint_builder/example/example_lint/pubspec.yaml new file mode 100644 index 00000000..1d835d8a --- /dev/null +++ b/packages/custom_lint_builder/example/example_lint/pubspec.yaml @@ -0,0 +1,11 @@ +name: custom_lint_builder_example_lint +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + custom_lint_builder: + path: ../../../custom_lint_builder diff --git a/packages/custom_lint_builder/example/example_lint/pubspec_overrides.yaml b/packages/custom_lint_builder/example/example_lint/pubspec_overrides.yaml new file mode 100644 index 00000000..ee6fd0f4 --- /dev/null +++ b/packages/custom_lint_builder/example/example_lint/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_visitor +dependency_overrides: + custom_lint: + path: ../../../custom_lint + custom_lint_builder: + path: ../.. + custom_lint_core: + path: ../../../custom_lint_core diff --git a/packages/custom_lint_builder/example/lib/main.dart b/packages/custom_lint_builder/example/lib/main.dart new file mode 100644 index 00000000..2f87b2af --- /dev/null +++ b/packages/custom_lint_builder/example/lib/main.dart @@ -0,0 +1,15 @@ +import 'package:riverpod/riverpod.dart'; + +void main() { + print('hello world'); +} + +class Main {} + +// expect_lint: riverpod_final_provider +ProviderBase provider = Provider((ref) => 0); + +// expect_lint: riverpod_final_provider +Provider provider2 = Provider((ref) => 0); + +Object? foo = 42; diff --git a/packages/custom_lint_builder/example/pubspec.yaml b/packages/custom_lint_builder/example/pubspec.yaml new file mode 100644 index 00000000..2113819e --- /dev/null +++ b/packages/custom_lint_builder/example/pubspec.yaml @@ -0,0 +1,13 @@ +name: custom_lint_builder_example_app +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + riverpod: ^2.0.0 + +dev_dependencies: + custom_lint: + custom_lint_builder_example_lint: + path: ./example_lint diff --git a/packages/custom_lint_builder/example/pubspec_overrides.yaml b/packages/custom_lint_builder/example/pubspec_overrides.yaml new file mode 100644 index 00000000..091a138f --- /dev/null +++ b/packages/custom_lint_builder/example/pubspec_overrides.yaml @@ -0,0 +1,10 @@ +# melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_builder_example_lint,custom_lint_core,custom_lint_visitor +dependency_overrides: + custom_lint: + path: ../../custom_lint + custom_lint_builder: + path: .. + custom_lint_builder_example_lint: + path: example_lint + custom_lint_core: + path: ../../custom_lint_core diff --git a/packages/custom_lint_builder/lib/custom_lint_builder.dart b/packages/custom_lint_builder/lib/custom_lint_builder.dart index ef37a116..ca757727 100644 --- a/packages/custom_lint_builder/lib/custom_lint_builder.dart +++ b/packages/custom_lint_builder/lib/custom_lint_builder.dart @@ -1,37 +1 @@ -import 'dart:async'; -import 'dart:isolate'; - -import 'package:analyzer_plugin/protocol/protocol.dart'; -import 'package:analyzer_plugin/protocol/protocol_generated.dart'; -import 'package:custom_lint/protocol.dart'; - -import 'src/analyzer_plugin/client.dart'; -import 'src/analyzer_plugin/isolate_channel.dart'; -import 'src/plugin_base.dart'; - -export 'src/plugin_base.dart' show PluginBase; - -void startPlugin(SendPort sendPort, PluginBase plugin) { - void send(Map json) { - sendPort.send(json); - } - - runZonedGuarded( - () { - final client = Client(plugin); - client.start(PluginIsolateChannel(sendPort)); - }, - (err, stack) => send( - PluginErrorParams( - false, - err.toString(), - stack.toString(), - ).toNotification().toJson(), - ), - zoneSpecification: ZoneSpecification( - print: (self, zoneDelegate, zone, message) { - send(PrintParams(message).toNotification().toJson()); - }, - ), - ); -} +export 'package:custom_lint_core/custom_lint_core.dart'; diff --git a/packages/custom_lint_builder/lib/src/analyzer_plugin/client.dart b/packages/custom_lint_builder/lib/src/analyzer_plugin/client.dart deleted file mode 100644 index 7f641535..00000000 --- a/packages/custom_lint_builder/lib/src/analyzer_plugin/client.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:analyzer/dart/analysis/context_locator.dart' as analyzer; -import 'package:analyzer/dart/analysis/context_root.dart' as analyzer; -import 'package:analyzer/dart/analysis/results.dart' as analyzer; -import 'package:analyzer/file_system/file_system.dart'; -import 'package:analyzer/file_system/file_system.dart' as analyzer; -// ignore: implementation_imports -import 'package:analyzer/src/dart/analysis/context_builder.dart' as analyzer; -// ignore: implementation_imports -import 'package:analyzer/src/dart/analysis/driver.dart'; -import 'package:analyzer_plugin/protocol/protocol.dart' as analyzer_plugin; -import 'package:analyzer_plugin/protocol/protocol_common.dart' - as analyzer_plugin; -import 'package:analyzer_plugin/protocol/protocol_generated.dart' - as analyzer_plugin; - -import '../plugin_base.dart'; -import 'plugin_client.dart'; - -class Client extends ClientPlugin { - Client(this.plugin, [ResourceProvider? provider]) : super(provider); - - final PluginBase plugin; - - @override - List get fileGlobsToAnalyze => ['*.g.dart']; - - @override - String get name => 'foo'; - - @override - String get version => '1.0.0-alpha.0'; - - @override - AnalysisDriver createAnalysisDriver( - analyzer_plugin.ContextRoot pluginContextRoot, - ) { - print('createAnalysisDriver'); - final contextRoot = pluginContextRoot.asAnalayzerContextRoot( - resourceProvider: resourceProvider, - ); - - final builder = analyzer.ContextBuilderImpl( - resourceProvider: resourceProvider, - ); - final context = builder.createContext(contextRoot: contextRoot); - -// TODO cancel sub - context.driver.results.listen((analysisResult) { - if (analysisResult is analyzer.ResolvedUnitResult) { - print('result: ${analysisResult.path} for ${pluginContextRoot.root}'); - - if (analysisResult.exists) { - channel.sendNotification( - analyzer_plugin.AnalysisErrorsParams( - analysisResult.path, - // TODO handle error - plugin.getLints(analysisResult.libraryElement).toList(), - ).toNotification(), - ); - } - } else if (analysisResult is analyzer.ErrorsResult) { - print('error at ${analysisResult.path} for ${pluginContextRoot.root}'); - } else { - print('StateError $analysisResult'); - } - }); - - return context.driver; - } - - @override - Future handleEditGetFixes( - analyzer_plugin.EditGetFixesParams parameters, - ) async { - final result = - await (this.driverForPath(parameters.file) as AnalysisDriver?) - ?.getResult(parameters.file); - if (result != null && result is analyzer.ResolvedUnitResult) { - return analyzer_plugin.EditGetFixesResult( - plugin - .getFixes( - result.libraryElement, - parameters.offset, - ) - .toList(), - ); - } - return super.handleEditGetFixes(parameters); - } -} - -extension on analyzer_plugin.ContextRoot { - analyzer.ContextRoot asAnalayzerContextRoot({ - required analyzer.ResourceProvider resourceProvider, - }) { - final locator = - analyzer.ContextLocator(resourceProvider: resourceProvider).locateRoots( - includedPaths: [root], - excludedPaths: exclude, - optionsFile: optionsFile, - ); - - return locator.single; - } -} diff --git a/packages/custom_lint_builder/lib/src/analyzer_plugin/plugin_client.dart b/packages/custom_lint_builder/lib/src/analyzer_plugin/plugin_client.dart deleted file mode 100644 index 34c43214..00000000 --- a/packages/custom_lint_builder/lib/src/analyzer_plugin/plugin_client.dart +++ /dev/null @@ -1,630 +0,0 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/file_system/file_system.dart'; -import 'package:analyzer/file_system/overlay_file_system.dart'; -import 'package:analyzer/file_system/physical_file_system.dart'; -import 'package:analyzer/src/dart/analysis/byte_store.dart'; -import 'package:analyzer/src/dart/analysis/driver.dart' - show AnalysisDriver, AnalysisDriverGeneric, AnalysisDriverScheduler; -import 'package:analyzer/src/dart/analysis/file_byte_store.dart'; -import 'package:analyzer/src/dart/analysis/performance_logger.dart'; -import 'package:analyzer/src/generated/sdk.dart'; -import 'package:analyzer_plugin/channel/channel.dart'; -import 'package:analyzer_plugin/protocol/protocol.dart'; -import 'package:analyzer_plugin/protocol/protocol_common.dart'; -import 'package:analyzer_plugin/protocol/protocol_constants.dart'; -import 'package:analyzer_plugin/protocol/protocol_generated.dart'; -import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'; -import 'package:analyzer_plugin/src/utilities/null_string_sink.dart'; -import 'package:analyzer_plugin/utilities/subscriptions/subscription_manager.dart'; -import 'package:pub_semver/pub_semver.dart'; - -/// The abstract superclass of any class implementing a plugin for the analysis -/// server. -/// -/// Clients may not implement or mix-in this class, but are expected to extend -/// it. -abstract class ClientPlugin { - /// Initialize a newly created analysis server plugin. If a resource [provider] - /// is given, then it will be used to access the file system. Otherwise a - /// resource provider that accesses the physical file system will be used. - ClientPlugin(ResourceProvider? provider) - : resourceProvider = OverlayResourceProvider( - provider ?? PhysicalResourceProvider.INSTANCE) { - analysisDriverScheduler = AnalysisDriverScheduler(performanceLog); - analysisDriverScheduler.start(); - } - - /// A megabyte. - static const int M = 1024 * 1024; - - /// The communication channel being used to communicate with the analysis - /// server. - late PluginCommunicationChannel _channel; - - /// The resource provider used to access the file system. - final OverlayResourceProvider resourceProvider; - - /// The next modification stamp for a changed file in the [resourceProvider]. - int _overlayModificationStamp = 0; - - /// The object used to manage analysis subscriptions. - final SubscriptionManager subscriptionManager = SubscriptionManager(); - - /// The scheduler used by any analysis drivers that are created. - late AnalysisDriverScheduler analysisDriverScheduler; - - /// A table mapping the current context roots to the analysis driver created - /// for that root. - final Map driverMap = - {}; - - /// The performance log used by any analysis drivers that are created. - final PerformanceLog performanceLog = PerformanceLog(NullStringSink()); - - /// The byte store used by any analysis drivers that are created, or `null` if - /// the cache location isn't known because the 'plugin.version' request has not - /// yet been received. - late ByteStore _byteStore; - - /// The SDK manager used to manage SDKs. - late DartSdkManager _sdkManager; - - /// Return the byte store used by any analysis drivers that are created, or - /// `null` if the cache location isn't known because the 'plugin.version' - /// request has not yet been received. - ByteStore get byteStore => _byteStore; - - /// Return the communication channel being used to communicate with the - /// analysis server, or `null` if the plugin has not been started. - PluginCommunicationChannel get channel => _channel; - - /// Return the user visible information about how to contact the plugin authors - /// with any problems that are found, or `null` if there is no contact info. - String? get contactInfo => null; - - /// Return a list of glob patterns selecting the files that this plugin is - /// interested in analyzing. - List get fileGlobsToAnalyze; - - /// Return the user visible name of this plugin. - String get name; - - /// Return the SDK manager used to manage SDKs. - DartSdkManager get sdkManager => _sdkManager; - - /// Return the version number of the plugin spec required by this plugin, - /// encoded as a string. - String get version; - - /// Handle the fact that the file with the given [path] has been modified. - void contentChanged(String path) { - driverForPath(path)?.addFile(path); - } - - /// Return the context root containing the file at the given [filePath]. - ContextRoot? contextRootContaining(String filePath) { - final pathContext = resourceProvider.pathContext; - - /// Return `true` if the given [child] is either the same as or within the - /// given [parent]. - bool isOrWithin(String parent, String child) { - return parent == child || pathContext.isWithin(parent, child); - } - - /// Return `true` if the given context [root] contains the target [file]. - bool ownsFile(ContextRoot root) { - if (isOrWithin(root.root, filePath)) { - final excludedPaths = root.exclude; - for (final excludedPath in excludedPaths) { - if (isOrWithin(excludedPath, filePath)) { - return false; - } - } - return true; - } - return false; - } - - for (final root in driverMap.keys) { - if (ownsFile(root)) { - return root; - } - } - return null; - } - - /// Create an analysis driver that can analyze the files within the given - /// [contextRoot]. - AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot); - - /// Return the driver being used to analyze the file with the given [path]. - AnalysisDriverGeneric? driverForPath(String path) { - final contextRoot = contextRootContaining(path); - if (contextRoot == null) { - return null; - } - return driverMap[contextRoot]; - } - - /// Return the result of analyzing the file with the given [path]. - /// - /// Throw a [RequestFailure] is the file cannot be analyzed or if the driver - /// associated with the file is not an [AnalysisDriver]. - Future getResolvedUnitResult(String path) async { - final driver = driverForPath(path); - if (driver is! AnalysisDriver) { - // Return an error from the request. - throw RequestFailure( - RequestErrorFactory.pluginError('Failed to analyze $path', null)); - } - final result = await driver.getResult(path); - if (result is! ResolvedUnitResult) { - // Return an error from the request. - throw RequestFailure( - RequestErrorFactory.pluginError('Failed to analyze $path', null)); - } - return result; - } - - /// Handle an 'analysis.getNavigation' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleAnalysisGetNavigation( - AnalysisGetNavigationParams params) async { - return AnalysisGetNavigationResult( - [], [], []); - } - - /// Handle an 'analysis.handleWatchEvents' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleAnalysisHandleWatchEvents( - AnalysisHandleWatchEventsParams parameters) async { - for (final event in parameters.events) { - switch (event.type) { - case WatchEventType.ADD: - // TODO(brianwilkerson) Handle the event. - break; - case WatchEventType.MODIFY: - contentChanged(event.path); - break; - case WatchEventType.REMOVE: - // TODO(brianwilkerson) Handle the event. - break; - default: - // Ignore unhandled watch event types. - break; - } - } - return AnalysisHandleWatchEventsResult(); - } - - /// Handle an 'analysis.setContextRoots' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleAnalysisSetContextRoots( - AnalysisSetContextRootsParams parameters, - ) async { - final contextRoots = parameters.roots; - final oldRoots = driverMap.keys.toList(); - for (final contextRoot in contextRoots) { - if (!oldRoots.remove(contextRoot)) { - // The context is new, so we create a driver for it. Creating the driver - // has the side-effect of adding it to the analysis driver scheduler. - final driver = createAnalysisDriver(contextRoot); - driverMap[contextRoot] = driver; - _addFilesToDriver( - driver, - resourceProvider.getResource(contextRoot.root), - contextRoot.exclude); - } - } - for (final contextRoot in oldRoots) { - // The context has been removed, so we remove its driver. - final driver = driverMap.remove(contextRoot); - // The `dispose` method has the side-effect of removing the driver from - // the analysis driver scheduler. - driver?.dispose(); - } - - final filesToFullyResolve = { - // ... all other files need to be analyzed, but don't trump priority - for (final driver2 in driverMap.values) - ...(driver2 as AnalysisDriver).addedFiles, - }; - - handleAnalysisSetPriorityFiles( - AnalysisSetPriorityFilesParams(filesToFullyResolve.toList()), - ); - - return AnalysisSetContextRootsResult(); - } - - /// Handle an 'analysis.setPriorityFiles' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - AnalysisSetPriorityFilesResult handleAnalysisSetPriorityFiles( - AnalysisSetPriorityFilesParams parameters, - ) { - final files = parameters.files; - final filesByDriver = >{}; - for (final file in files) { - final contextRoot = contextRootContaining(file); - if (contextRoot != null) { - // TODO(brianwilkerson) Which driver should we use if there is no context root? - final driver = driverMap[contextRoot]!; - filesByDriver.putIfAbsent(driver, () => []).add(file); - } - } - filesByDriver.forEach((AnalysisDriverGeneric driver, List files) { - driver.priorityFiles = files; - }); - return AnalysisSetPriorityFilesResult(); - } - - /// Handle an 'analysis.setSubscriptions' request. Most subclasses should not - /// override this method, but should instead use the [subscriptionManager] to - /// access the list of subscriptions for any given file. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleAnalysisSetSubscriptions( - AnalysisSetSubscriptionsParams parameters) async { - final subscriptions = parameters.subscriptions; - final newSubscriptions = - subscriptionManager.setSubscriptions(subscriptions); - sendNotificationsForSubscriptions(newSubscriptions); - return AnalysisSetSubscriptionsResult(); - } - - /// Handle an 'analysis.updateContent' request. Most subclasses should not - /// override this method, but should instead use the [contentCache] to access - /// the current content of overlaid files. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleAnalysisUpdateContent( - AnalysisUpdateContentParams parameters) async { - final files = parameters.files; - files.forEach((String filePath, Object? overlay) { - // Prepare the old overlay contents. - String? oldContents; - try { - if (resourceProvider.hasOverlay(filePath)) { - final file = resourceProvider.getFile(filePath); - oldContents = file.readAsStringSync(); - } - } catch (_) {} - - // Prepare the new contents. - String? newContents; - if (overlay is AddContentOverlay) { - newContents = overlay.content; - } else if (overlay is ChangeContentOverlay) { - if (oldContents == null) { - // The server should only send a ChangeContentOverlay if there is - // already an existing overlay for the source. - throw RequestFailure( - RequestErrorFactory.invalidOverlayChangeNoContent()); - } - try { - newContents = SourceEdit.applySequence(oldContents, overlay.edits); - } on RangeError { - throw RequestFailure( - RequestErrorFactory.invalidOverlayChangeInvalidEdit()); - } - } else if (overlay is RemoveContentOverlay) { - newContents = null; - } - - if (newContents != null) { - resourceProvider.setOverlay( - filePath, - content: newContents, - modificationStamp: _overlayModificationStamp++, - ); - } else { - resourceProvider.removeOverlay(filePath); - } - - contentChanged(filePath); - }); - return AnalysisUpdateContentResult(); - } - - /// Handle a 'completion.getSuggestions' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleCompletionGetSuggestions( - CompletionGetSuggestionsParams parameters) async { - return CompletionGetSuggestionsResult( - -1, -1, const []); - } - - /// Handle an 'edit.getAssists' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleEditGetAssists( - EditGetAssistsParams parameters) async { - return EditGetAssistsResult(const []); - } - - /// Handle an 'edit.getAvailableRefactorings' request. Subclasses that override - /// this method in order to participate in refactorings must also override the - /// method [handleEditGetRefactoring]. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleEditGetAvailableRefactorings( - EditGetAvailableRefactoringsParams parameters) async { - return EditGetAvailableRefactoringsResult(const []); - } - - /// Handle an 'edit.getFixes' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleEditGetFixes( - EditGetFixesParams parameters) async { - return EditGetFixesResult(const []); - } - - /// Handle an 'edit.getRefactoring' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleEditGetRefactoring( - EditGetRefactoringParams parameters) async { - return null; - } - - /// Handle a 'kythe.getKytheEntries' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handleKytheGetKytheEntries( - KytheGetKytheEntriesParams parameters) async { - return null; - } - - /// Handle a 'plugin.shutdown' request. Subclasses can override this method to - /// perform any required clean-up, but cannot prevent the plugin from shutting - /// down. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handlePluginShutdown( - PluginShutdownParams parameters) async { - return PluginShutdownResult(); - } - - /// Handle a 'plugin.versionCheck' request. - /// - /// Throw a [RequestFailure] if the request could not be handled. - Future handlePluginVersionCheck( - PluginVersionCheckParams parameters) async { - final byteStorePath = parameters.byteStorePath; - final sdkPath = parameters.sdkPath; - final versionString = parameters.version; - final serverVersion = Version.parse(versionString); - _byteStore = MemoryCachingByteStore( - FileByteStore(byteStorePath, - tempNameSuffix: DateTime.now().millisecondsSinceEpoch.toString()), - 64 * M); - _sdkManager = DartSdkManager(sdkPath); - return PluginVersionCheckResult( - isCompatibleWith(serverVersion), name, version, fileGlobsToAnalyze, - contactInfo: contactInfo); - } - - /// Return `true` if this plugin is compatible with an analysis server that is - /// using the given version of the plugin API. - bool isCompatibleWith(Version serverVersion) => - serverVersion <= Version.parse(version); - - /// The method that is called when the analysis server closes the communication - /// channel. This method will not be invoked under normal conditions because - /// the server will send a shutdown request and the plugin will stop listening - /// to the channel before the server closes the channel. - void onDone() {} - - /// The method that is called when an error has occurred in the analysis - /// server. This method will not be invoked under normal conditions. - void onError(Object exception, StackTrace stackTrace) {} - - /// If the plugin provides folding information, send a folding notification - /// for the file with the given [path] to the server. - Future sendFoldingNotification(String path) { - return Future.value(); - } - - /// If the plugin provides highlighting information, send a highlights - /// notification for the file with the given [path] to the server. - Future sendHighlightsNotification(String path) { - return Future.value(); - } - - /// If the plugin provides navigation information, send a navigation - /// notification for the file with the given [path] to the server. - Future sendNavigationNotification(String path) { - return Future.value(); - } - - /// Send notifications for the services subscribed to for the file with the - /// given [path]. - /// - /// This is a convenience method that subclasses can use to send notifications - /// after analysis has been performed on a file. - void sendNotificationsForFile(String path) { - for (final service in subscriptionManager.servicesForFile(path)) { - _sendNotificationForFile(path, service); - } - } - - /// Send notifications corresponding to the given description of - /// [subscriptions]. The map is keyed by the path of each file for which - /// notifications should be sent and has values representing the list of - /// services associated with the notifications to send. - /// - /// This method is used when the set of subscribed notifications has been - /// changed and notifications need to be sent even when the specified files - /// have already been analyzed. - void sendNotificationsForSubscriptions( - Map> subscriptions) { - subscriptions.forEach((String path, List services) { - for (final service in services) { - _sendNotificationForFile(path, service); - } - }); - } - - /// If the plugin provides occurrences information, send an occurrences - /// notification for the file with the given [path] to the server. - Future sendOccurrencesNotification(String path) { - return Future.value(); - } - - /// If the plugin provides outline information, send an outline notification - /// for the file with the given [path] to the server. - Future sendOutlineNotification(String path) { - return Future.value(); - } - - /// Start this plugin by listening to the given communication [channel]. - void start(PluginCommunicationChannel channel) { - _channel = channel; - _channel.listen(_onRequest, onError: onError, onDone: onDone); - } - - /// Add all of the files contained in the given [resource] that are not in the - /// list of [excluded] resources to the given [driver]. - void _addFilesToDriver( - AnalysisDriverGeneric driver, Resource resource, List excluded) { - final path = resource.path; - if (excluded.contains(path)) { - return; - } - if (resource is File) { - driver.addFile(path); - } else if (resource is Folder) { - try { - for (final child in resource.getChildren()) { - _addFilesToDriver(driver, child, excluded); - } - } on FileSystemException { - // The folder does not exist, so ignore it. - } - } - } - - /// Compute the response that should be returned for the given [request], or - /// `null` if the response has already been sent. - Future _getResponse(Request request, int requestTime) async { - ResponseResult? result; - switch (request.method) { - case ANALYSIS_REQUEST_GET_NAVIGATION: - final params = AnalysisGetNavigationParams.fromRequest(request); - result = await handleAnalysisGetNavigation(params); - break; - case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: - final params = AnalysisHandleWatchEventsParams.fromRequest(request); - result = await handleAnalysisHandleWatchEvents(params); - break; - case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: - final params = AnalysisSetContextRootsParams.fromRequest(request); - result = await handleAnalysisSetContextRoots(params); - break; - case ANALYSIS_REQUEST_SET_PRIORITY_FILES: - final params = AnalysisSetPriorityFilesParams.fromRequest(request); - result = handleAnalysisSetPriorityFiles(params); - break; - case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS: - final params = AnalysisSetSubscriptionsParams.fromRequest(request); - result = await handleAnalysisSetSubscriptions(params); - break; - case ANALYSIS_REQUEST_UPDATE_CONTENT: - final params = AnalysisUpdateContentParams.fromRequest(request); - result = await handleAnalysisUpdateContent(params); - break; - case COMPLETION_REQUEST_GET_SUGGESTIONS: - final params = CompletionGetSuggestionsParams.fromRequest(request); - result = await handleCompletionGetSuggestions(params); - break; - case EDIT_REQUEST_GET_ASSISTS: - final params = EditGetAssistsParams.fromRequest(request); - result = await handleEditGetAssists(params); - break; - case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS: - final params = EditGetAvailableRefactoringsParams.fromRequest(request); - result = await handleEditGetAvailableRefactorings(params); - break; - case EDIT_REQUEST_GET_FIXES: - final params = EditGetFixesParams.fromRequest(request); - result = await handleEditGetFixes(params); - break; - case EDIT_REQUEST_GET_REFACTORING: - final params = EditGetRefactoringParams.fromRequest(request); - result = await handleEditGetRefactoring(params); - break; - case KYTHE_REQUEST_GET_KYTHE_ENTRIES: - final params = KytheGetKytheEntriesParams.fromRequest(request); - result = await handleKytheGetKytheEntries(params); - break; - case PLUGIN_REQUEST_SHUTDOWN: - final params = PluginShutdownParams(); - result = await handlePluginShutdown(params); - _channel.sendResponse(result.toResponse(request.id, requestTime)); - _channel.close(); - return null; - case PLUGIN_REQUEST_VERSION_CHECK: - final params = PluginVersionCheckParams.fromRequest(request); - result = await handlePluginVersionCheck(params); - break; - } - if (result == null) { - return Response(request.id, requestTime, - error: RequestErrorFactory.unknownRequest(request.method)); - } - return result.toResponse(request.id, requestTime); - } - - /// The method that is called when a [request] is received from the analysis - /// server. - Future _onRequest(Request request) async { - final requestTime = DateTime.now().millisecondsSinceEpoch; - final id = request.id; - Response? response; - try { - response = await _getResponse(request, requestTime); - } on RequestFailure catch (exception) { - response = Response(id, requestTime, error: exception.error); - } catch (exception, stackTrace) { - response = Response(id, requestTime, - error: RequestError( - RequestErrorCode.PLUGIN_ERROR, exception.toString(), - stackTrace: stackTrace.toString())); - } - if (response != null) { - _channel.sendResponse(response); - } - } - - /// Send a notification for the file at the given [path] corresponding to the - /// given [service]. - void _sendNotificationForFile(String path, AnalysisService service) { - switch (service) { - case AnalysisService.FOLDING: - sendFoldingNotification(path); - break; - case AnalysisService.HIGHLIGHTS: - sendHighlightsNotification(path); - break; - case AnalysisService.NAVIGATION: - sendNavigationNotification(path); - break; - case AnalysisService.OCCURRENCES: - sendOccurrencesNotification(path); - break; - case AnalysisService.OUTLINE: - sendOutlineNotification(path); - break; - } - } -} diff --git a/packages/custom_lint_builder/lib/src/channel.dart b/packages/custom_lint_builder/lib/src/channel.dart new file mode 100644 index 00000000..0002df69 --- /dev/null +++ b/packages/custom_lint_builder/lib/src/channel.dart @@ -0,0 +1,178 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:analyzer_plugin/protocol/protocol.dart'; +// ignore: implementation_imports, tight versioning with custom_lint +import 'package:custom_lint/src/async_operation.dart'; +// ignore: implementation_imports, tight versioning with custom_lint +import 'package:custom_lint/src/channels.dart'; +// ignore: implementation_imports, tight versioning with custom_lint +import 'package:custom_lint/src/v2/protocol.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; + +import 'client.dart'; + +/// Converts a Stream/Sink into a SendPort/ReceivePort equivalent +class StreamToSentPortAdapter { + /// Converts a Stream/Sink into a SendPort/ReceivePort equivalent + StreamToSentPortAdapter( + Stream> input, + void Function(Map output) output, { + required void Function() onDone, + }) { + final Stream outputStream = _outputReceivePort.asBroadcastStream(); + final inputSendPort = + outputStream.where((e) => e is SendPort).cast().safeFirst; + + final sub = outputStream + .where((e) => e is! SendPort) + .map((e) => e! as Map) + .listen((e) => output(e)); + + input.listen( + (e) { + unawaited(inputSendPort.then((value) => value.send(e))); + }, + onDone: () { + _outputReceivePort.close(); + unawaited(sub.cancel()); + onDone(); + }, + ); + } + + /// The [SendPort] associated with the input [Stream]. + SendPort get sendPort => _outputReceivePort.sendPort; + + // TODO appears to sometime not be closed. + // Could be because of the `onDone` callback not being invoked. + final _outputReceivePort = ReceivePort(); +} + +/// The prototype of plugin's `createPlugin` entrypoint function. +typedef CreatePluginMain = PluginBase Function(); + +/// Starts a custom_lint client using web sockets. +Future runSocket( + Map pluginMains, { + required int port, + required String host, + required bool fix, + required bool includeBuiltInLints, +}) async { + late Future client; + + await asyncRunZonedGuarded( + () => client = Future(() async { + // ignore: close_sinks, connection stays open until the plugin is killed + final socket = await Socket.connect(host, port); + final socketChannel = JsonSocketChannel(Future.value(socket)); + final registeredPlugins = {}; + + for (final main in pluginMains.entries) { + Zone.current.runGuarded( + () => registeredPlugins[main.key] = main.value(), + ); + } + + return CustomLintPluginClient( + includeBuiltInLints: includeBuiltInLints, + fix: fix, + _SocketCustomLintClientChannel( + socketChannel, + registeredPlugins, + onDone: () { + // If the server somehow quit, forcibly stop the client. + // In theory it should stop naturally, but let's make sure of this to prevent leaks. + // Tried with `socket.done.then` but it somehow was never invoked + exit(0); + }, + ), + ); + }), + (error, stackTrace) { + client.then((value) => value.handleError(error, stackTrace)); + }, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) { + client.then((value) => value.handlePrint(line)); + }, + ), + ); +} + +/// An interface for clients to send messages to the custom_lint server. +abstract class CustomLintClientChannel { + /// An interface for clients to send messages to the custom_lint server. + CustomLintClientChannel(this.registeredPlugins); + + /// The [SendPort] that will be passed to analyzer_plugin + SendPort get sendPort; + + /// The list of plugins installed by custom_lint. + final Map registeredPlugins; + + /// Messages from the custom_lint server + Stream get input; + + void _sendJson(Map json); + + /// Sends a response to the custom_lint server, associated to a request + void sendResponse(CustomLintResponse response) { + _sendJson(CustomLintMessage.response(response).toJson()); + } + + /// Sends a notification to the custom_lint server, which is not associated with + /// a request. + void sendEvent(CustomLintEvent event) { + _sendJson(CustomLintMessage.event(event).toJson()); + } +} + +class _SocketCustomLintClientChannel extends CustomLintClientChannel { + _SocketCustomLintClientChannel( + this.socket, + super.registeredPlugins, { + required this.onDone, + }); + + @override + SendPort get sendPort => _adapter.sendPort; + + final void Function() onDone; + final JsonSocketChannel socket; + + late final StreamToSentPortAdapter _adapter = StreamToSentPortAdapter( + onDone: onDone, + input + .where((e) => e is CustomLintRequestAnalyzerPluginRequest) + .cast() + .map((event) => event.request.toJson()), + (analyzerPluginOutput) { + if (analyzerPluginOutput.containsKey(Notification.EVENT)) { + sendEvent( + CustomLintEvent.analyzerPluginNotification( + Notification.fromJson(analyzerPluginOutput), + ), + ); + } else { + final response = Response.fromJson(analyzerPluginOutput); + sendResponse( + CustomLintResponse.analyzerPluginResponse(response, id: response.id), + ); + } + }, + ); + + @override + late final Stream input = socket.messages + .cast>() + .map(CustomLintRequest.fromJson) + .asBroadcastStream(); + + @override + void _sendJson(Map json) { + unawaited(socket.sendJson(json)); + } +} diff --git a/packages/custom_lint_builder/lib/src/client.dart b/packages/custom_lint_builder/lib/src/client.dart new file mode 100644 index 00000000..fc8560b5 --- /dev/null +++ b/packages/custom_lint_builder/lib/src/client.dart @@ -0,0 +1,1243 @@ +// ignore_for_file: invalid_use_of_internal_member, using custom_lint_core utils + +import 'dart:async'; +import 'dart:developer' as dev; +import 'dart:io' as io; + +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/context_root.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/source/file_source.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/plugin/plugin.dart' as analyzer_plugin; +import 'package:analyzer_plugin/protocol/protocol_common.dart' + as analyzer_plugin; +import 'package:analyzer_plugin/protocol/protocol_generated.dart' + as analyzer_plugin; +import 'package:analyzer_plugin/starter.dart' as analyzer_plugin; +import 'package:collection/collection.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint/src/async_operation.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint/src/v2/protocol.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/change_reporter.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/fixes.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/plugin_base.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/resolver.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/runnable.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; +import 'package:glob/glob.dart'; +import 'package:hotreloader/hotreloader.dart'; +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart' show PackageConfig; +import 'package:path/path.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:rxdart/subjects.dart'; + +import '../custom_lint_builder.dart'; +import 'channel.dart'; +import 'custom_analyzer_converter.dart'; +import 'expect_lint.dart'; +import 'ignore.dart'; + +/// Analysis utilities for custom_lint +extension AnalysisSessionUtils on AnalysisContext { + /// Create a [CustomLintResolverImpl] for a file. + @internal + CustomLintResolverImpl? createResolverForFile(File file) { + if (!file.exists) return null; + final source = FileSource(file); + final lineInfo = LineInfo.fromContent(source.contents.data); + + return CustomLintResolverImpl( + () async => safeGetResolvedUnitResult(file.path), + lineInfo: lineInfo, + source: source, + path: file.path, + ); + } +} + +extension on AnalysisContext { + /// Obtains a [ResolvedUnitResult] for the given [path], while catching [InconsistentAnalysisException] and retrying. + @internal + Future safeGetResolvedUnitResult(String path) async { + for (var i = 0; i < 5; i++) { + try { + final result = await currentSession.getResolvedUnit(path); + return result as ResolvedUnitResult; + } on InconsistentAnalysisException { + // Retry analysis on InconsistentAnalysisException + await applyPendingFileChanges(); + } + } + throw StateError('Failed to get resolved unit result for $path'); + } +} + +Future _isVmServiceEnabled() async { + final serviceInfo = await dev.Service.getInfo(); + return serviceInfo.serverUri != null; +} + +extension on analyzer_plugin.AnalysisErrorFixes { + ({String id, int priority})? findBatchFix(String filePath) { + final fixToBatch = fixes + .where((change) => change.canBatchFix(filePath)) + // Only a single fix at a time can be batched. + .singleOrNull; + if (fixToBatch == null) return null; + final id = fixToBatch.change.id; + if (id == null) return null; + + return ( + id: id, + priority: fixToBatch.priority, + ); + } +} + +extension on analyzer_plugin.PrioritizedSourceChange { + bool canBatchFix(String filePath) { + return !isIgnoreChange && + change.edits.every((element) => element.file == filePath); + } + + bool get isIgnoreChange { + return change.id == IgnoreCode.ignoreId; + } +} + +/// The custom_lint client +class CustomLintPluginClient { + /// The custom_lint client + CustomLintPluginClient( + this._channel, { + required this.includeBuiltInLints, + required this.fix, + }) { + _analyzerPlugin = _ClientAnalyzerPlugin( + _channel, + this, + resourceProvider: PhysicalResourceProvider.INSTANCE, + ); + _hotReloader = Future(_maybeStartHotLoad); + final starter = analyzer_plugin.ServerPluginStarter(_analyzerPlugin); + starter.start(_channel.sendPort); + + _channelInputSub = _channel.input.listen(_handleCustomLintRequest); + } + + /// Whether to include lints automatically by custom_lint. + /// This includes: + /// - Errors at the top of the file when a lint threw + final bool includeBuiltInLints; + + /// Whether to attempt fixing analysis issues before reporting them. + final bool fix; + late final StreamSubscription _channelInputSub; + late final Future _hotReloader; + final CustomLintClientChannel _channel; + late final _ClientAnalyzerPlugin _analyzerPlugin; + + var _contextRootsForPlugin = >{}; + + Future _maybeStartHotLoad() async { + if (!await _isVmServiceEnabled()) return null; + return HotReloader.create( + onAfterReload: (value) { + switch (value.result) { + case HotReloadResult.Succeeded: + case HotReloadResult.PartiallySucceeded: + _analyzerPlugin.reAnalyze(); + default: + } + }, + ); + } + + bool _isPluginActiveForContextRoot( + AnalysisContext analysisContext, { + required String pluginName, + }) { + final contextRootsForPlugin = _contextRootsForPlugin[pluginName]; + if (contextRootsForPlugin == null) { + return false; + } + + return contextRootsForPlugin.any( + (contextRootForPlugin) => + analysisContext.contextRoot.root == contextRootForPlugin.root, + ); + } + + Future _handleCustomLintRequest(CustomLintRequest request) async { + try { + // Analyzer_plugin requests are handles by the _analyzer_plugin client + final CustomLintResponse? response; + switch (request) { + case CustomLintRequestAnalyzerPluginRequest(): + response = null; + case CustomLintRequestAwaitAnalysisDone(:final reload): + await _analyzerPlugin._awaitAnalysisDone(reload: reload); + response = CustomLintResponse.awaitAnalysisDone(id: request.id); + case CustomLintRequestPing(): + response = CustomLintResponse.pong(id: request.id); + } + + if (response != null) { + _channel.sendResponse(response); + } + } catch (err, stack) { + _channel.sendResponse( + CustomLintResponse.error( + id: request.id, + message: err.toString(), + stackTrace: stack.toString(), + ), + ); + } + } + + Future _updateActivePluginList( + AnalysisContextCollection analysisContextCollection, + Map> pubspecs, + ) async { + _contextRootsForPlugin = {}; + + for (final analysisContext in analysisContextCollection.contexts) { + final pubspec = await pubspecs[analysisContext]!; + + for (final pluginName in _channel.registeredPlugins.keys) { + final isPluginEnabledInContext = + pubspec.dependencies.containsKey(pluginName) || + pubspec.devDependencies.containsKey(pluginName) || + pubspec.dependencyOverrides.containsKey(pluginName); + + if (isPluginEnabledInContext) { + final contextRootsForPlugin = + _contextRootsForPlugin[pluginName] ??= []; + contextRootsForPlugin.add(analysisContext.contextRoot); + } + } + } + } + + /// An event handler for uncaught errors. + /// + /// This method will be invoked for errors that are not associated with a lint/fix/assist. + void handleError(Object error, StackTrace stackTrace) { + _channel.sendEvent( + CustomLintEvent.error( + error.toString(), + stackTrace.toString(), + pluginName: null, + ), + ); + } + + /// An event handler for invocations to [print] in the client. + /// + /// This method will be invoked for prints that are not associated with a lint/fix/assist. + void handlePrint(String message) { + _channel.sendEvent(CustomLintEvent.print(message, pluginName: null)); + } + + Future _handlePluginShutdown() async { + await Future.wait([ + _channelInputSub.cancel(), + _hotReloader.catchError((_) => null).then((value) => value?.stop()), + ]); + } +} + +class _CustomLintAnalysisConfigs { + _CustomLintAnalysisConfigs( + this.configs, + this.rules, + this.fixes, + this.assists, + this.analysisContext, + this.pubspec, + ); + + factory _CustomLintAnalysisConfigs.from( + Pubspec pubspecForContext, + PackageConfig packageConfig, + AnalysisContext analysisContext, + CustomLintPluginClient client, + ) { + final configs = CustomLintConfigs.parse( + analysisContext.contextRoot.optionsFile, + packageConfig, + ); + + final activePluginsForContext = Map.fromEntries( + client._channel.registeredPlugins.entries.where( + (plugin) => client._isPluginActiveForContextRoot( + analysisContext, + pluginName: plugin.key, + ), + ), + ); + + final rules = _lintRulesForContext(activePluginsForContext, configs); + final fixes = _fixesForRules( + rules, + includeBuiltInLints: client.includeBuiltInLints, + ); + final assists = _assistsForContext( + activePluginsForContext, + configs, + client, + ); + + return _CustomLintAnalysisConfigs( + configs, + rules, + fixes, + assists, + analysisContext, + pubspecForContext, + ); + } + + static List _lintRulesForContext( + Map activePluginsForContext, + CustomLintConfigs configs, + ) { + return activePluginsForContext.entries + .expand((plugin) => plugin.value.getLintRules(configs)) + .where((lintRule) => lintRule.isEnabled(configs)) + .toList(); + } + + static Map> _fixesForRules( + List rules, { + required bool includeBuiltInLints, + }) { + return { + for (final rule in rules) + rule.code: [ + ...rule.getFixes(), + if (includeBuiltInLints) IgnoreCode(), + ], + }; + } + + static List _assistsForContext( + Map activePluginsForContext, + CustomLintConfigs configs, + CustomLintPluginClient client, + ) { + return activePluginsForContext.entries + .expand((plugin) => plugin.value.getAssists()) + .toList(); + } + + final CustomLintConfigs configs; + final List rules; + final Map> fixes; + final List assists; + final Pubspec pubspec; + final AnalysisContext analysisContext; +} + +@immutable +class _AnalysisErrorsKey { + const _AnalysisErrorsKey({ + required this.filePath, + required this.analysisContext, + }); + + final String filePath; + final AnalysisContext analysisContext; + + @override + bool operator ==(Object other) => + other is _AnalysisErrorsKey && + other.filePath == filePath && + other.analysisContext == analysisContext; + + @override + int get hashCode => Object.hash(filePath, analysisContext); +} + +class _FileContext { + _FileContext({ + required this.resolver, + required this.analysisContext, + required this.contextCollection, + required this.path, + required this.configs, + }) : key = _AnalysisErrorsKey( + filePath: path, + analysisContext: analysisContext, + ); + + final String path; + final _AnalysisErrorsKey key; + final CustomLintResolverImpl resolver; + final AnalysisContext analysisContext; + final AnalysisContextCollection contextCollection; + final _CustomLintAnalysisConfigs configs; +} + +class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { + _ClientAnalyzerPlugin( + this._channel, + this._client, { + required super.resourceProvider, + }); + + final CustomLintClientChannel _channel; + final CustomLintPluginClient _client; + final _contextCollection = BehaviorSubject(); + final _pendingOperations = >[]; + var _customLintConfigsForAnalysisContexts = + {}; + final _analysisErrorsForAnalysisContexts = + <_AnalysisErrorsKey, List>{}; + + @override + List get fileGlobsToAnalyze => ['*']; + + @override + String get name => 'custom_lint_client'; + + @override + String get version => '1.0.0-alpha.0'; + + Future<_FileContext?> _fileContext(String path) async { + final contextCollection = await _contextCollection.safeFirst; + final analysisContext = contextCollection.contextFor(path); + final resolver = analysisContext.createResolverForFile( + resourceProvider.getFile(path), + ); + + if (resolver == null) return null; + + final configs = _customLintConfigsForAnalysisContexts[analysisContext]; + if (configs == null) return null; + + return _FileContext( + path: path, + resolver: resolver, + analysisContext: analysisContext, + contextCollection: contextCollection, + configs: configs, + ); + } + + Future reAnalyze() async { + final contextCollection = _contextCollection.valueOrNull; + if (contextCollection != null) { + await afterNewContextCollection(contextCollection: contextCollection); + } + } + + @override + Future afterNewContextCollection({ + required AnalysisContextCollection contextCollection, + }) { + _contextCollection.add(contextCollection); + return _runOperation(() async { + // Clear lints as we got a new context collection + _analysisErrorsForAnalysisContexts.removeWhere( + (key, value) => + contextCollection.contexts.contains(key.analysisContext), + ); + + // Wait for hot reload to start. + // Otherwise tests may miss the first hot reload. + await _client._hotReloader; + + final pubspecs = >{}; + + for (final context in contextCollection.contexts) { + final pubspec = tryFindProjectDirectory( + io.Directory(context.contextRoot.root.path), + ); + + if (pubspec != null) { + pubspecs[context] = parsePubspec(pubspec); + } + } + + // Running before updating the configs as the config parsing depends + // on this operation. + await _client._updateActivePluginList(contextCollection, pubspecs); + + _customLintConfigsForAnalysisContexts = { + for (final pubspecEntry in pubspecs.entries) + pubspecEntry.key: _CustomLintAnalysisConfigs.from( + await pubspecEntry.value, + await parsePackageConfig(io.Directory.current), + pubspecEntry.key, + _client, + ), + }; + + return super.afterNewContextCollection( + contextCollection: contextCollection, + ); + }); + } + + @override + Future handleEditGetAssists( + analyzer_plugin.EditGetAssistsParams parameters, + ) async { + // TODO test + final contextCollection = await _contextCollection.safeFirst; + final analysisContext = contextCollection.contextFor(parameters.file); + final assists = + _customLintConfigsForAnalysisContexts[analysisContext]?.assists; + + final resolver = analysisContext.createResolverForFile( + resourceProvider.getFile(parameters.file), + ); + if (resolver == null || assists == null || assists.isEmpty) { + return analyzer_plugin.EditGetAssistsResult([]); + } + + final configs = _customLintConfigsForAnalysisContexts[analysisContext]; + if (configs == null) { + return analyzer_plugin.EditGetAssistsResult([]); + } + + final target = SourceRange(parameters.offset, parameters.length); + final postRunCallbacks = []; + // TODO implement verbose mode to log lint duration + final registry = NodeLintRegistry(LintRegistry(), enableTiming: false); + final sharedState = {}; + + final changeReporter = ChangeReporterImpl( + configs.analysisContext.currentSession, + resolver, + ); + + await Future.wait([ + for (final assist in configs.assists) + _runAssistStartup( + resolver, + assist, + CustomLintContext( + LintRuleNodeRegistry(registry, assist.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + configs.pubspec, + ), + target, + ), + ]); + await Future.wait([ + for (final assist in configs.assists) + _runAssistRun( + resolver, + assist, + CustomLintContext( + LintRuleNodeRegistry(registry, assist.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + configs.pubspec, + ), + changeReporter, + target, + ), + ]); + + runPostRunCallbacks(postRunCallbacks); + + return analyzer_plugin.EditGetAssistsResult( + await changeReporter.complete(), + ); + } + + Future _runAssistStartup( + CustomLintResolver resolver, + Assist assist, + CustomLintContext context, + SourceRange target, + ) async { + return _runLintZoned( + resolver, + () => assist.startUp(resolver, context, target), + name: assist.runtimeType.toString(), + ); + } + + Future _runAssistRun( + CustomLintResolver resolver, + Assist assist, + CustomLintContext context, + ChangeReporter changeReporter, + SourceRange target, + ) async { + return _runLintZoned( + resolver, + () => assist.run(resolver, changeReporter, context, target), + name: assist.runtimeType.toString(), + ); + } + + @override + Future handleEditGetFixes( + analyzer_plugin.EditGetFixesParams parameters, + ) async { + final context = await _fileContext(parameters.file); + if (context == null) return analyzer_plugin.EditGetFixesResult([]); + + final analysisErrorsForContext = + _analysisErrorsForAnalysisContexts[context.key] ?? const []; + + final fixes = await _computeFixes( + analysisErrorsForContext + .where((error) => error.sourceRange.contains(parameters.offset)) + .toList(), + context, + analysisErrorsForContext, + ); + + return analyzer_plugin.EditGetFixesResult( + fixes.expand((fixes) { + return [ + fixes.fix, + if (fixes.batchFixes case final batchFixes?) batchFixes, + ]; + }).toList(), + ); + } + + Future< + List< + ({ + analyzer_plugin.AnalysisErrorFixes? batchFixes, + analyzer_plugin.AnalysisErrorFixes fix + })>> _computeFixes( + List errorsToFix, + _FileContext context, + List analysisErrorsForContext, + ) async { + return Future.wait( + errorsToFix.map((error) async { + final toBatch = analysisErrorsForContext + .where((e) => e.errorCode == error.errorCode) + .toList(); + + final changeReporterBuilder = ChangeReporterBuilderImpl( + context.resolver, + context.configs.analysisContext.currentSession, + ); + + await _runFixes( + context, + error, + analysisErrorsForContext, + changeReporterBuilder: changeReporterBuilder, + ); + final fix = await changeReporterBuilder.completeAsFixes( + error, + context, + ); + + final batchFix = fix.findBatchFix(context.path); + if (batchFix == null || toBatch.length <= 1) { + return ( + fix: fix, + batchFixes: null, + ); + } + + final batchReporter = ChangeReporterImpl( + context.configs.analysisContext.currentSession, + context.resolver, + ); + + final batchReporterBuilder = BatchChangeReporterBuilder( + batchReporter.createChangeBuilder( + message: 'Fix all "${error.errorCode}"', + priority: batchFix.priority - 1, + ), + ); + + // Compute batch in sequential mode because ChangeBuilder requires it. + for (final toBatchError in toBatch) { + await _runFixes( + where: (fix) => fix.id == batchFix.id, + context, + toBatchError, + analysisErrorsForContext, + changeReporterBuilder: batchReporterBuilder, + sequential: true, + ); + } + + final batchFixes = + await batchReporterBuilder.completeAsFixes(error, context); + + return ( + fix: fix, + batchFixes: batchFixes, + ); + }), + ); + } + + Future _runFixes( + _FileContext context, + AnalysisError analysisError, + List allErrors, { + required ChangeReporterBuilder changeReporterBuilder, + bool sequential = false, + bool Function(Fix fix)? where, + }) async { + Iterable? fixesForError = + context.configs.fixes[analysisError.errorCode]; + if (fixesForError == null) return; + + if (where != null) { + fixesForError = fixesForError.where(where); + } + + final otherErrors = allErrors + .where( + (element) => + element != analysisError && + element.errorCode == analysisError.errorCode, + ) + .toList(); + + await _run( + context, + fixesForError.map((fix) { + return ( + runnable: fix, + args: ( + reporter: changeReporterBuilder.createChangeReporter(id: fix.id), + analysisError: analysisError, + others: otherErrors, + ) + ); + }), + sequential: sequential, + ); + + await changeReporterBuilder.waitForCompletion(); + } + + Future _run( + _FileContext context, + Iterable<({Runnable runnable, ArgsT args})> allRunnables, { + bool sequential = false, + }) async { + // TODO implement verbose mode to log lint duration + + final bundledRunnables = + sequential ? allRunnables.map((e) => [e]).toList() : [allRunnables]; + + for (final runnableBundle in bundledRunnables) { + final registry = NodeLintRegistry(LintRegistry(), enableTiming: false); + final postRunCallbacks = []; + final sharedState = {}; + + await Future.wait([ + for (final (:runnable, args: _) in runnableBundle) + _runLintZoned( + context.resolver, + () => runnable.startUp( + context.resolver, + CustomLintContext( + LintRuleNodeRegistry(registry, runnable.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + context.configs.pubspec, + ), + ), + name: runnable.runtimeType.toString(), + ), + ]); + + await Future.wait([ + for (final (:runnable, :args) in runnableBundle) + _runLintZoned( + context.resolver, + () => runnable.callRun( + context.resolver, + CustomLintContext( + LintRuleNodeRegistry(registry, runnable.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + context.configs.pubspec, + ), + args, + ), + name: runnable.runtimeType.toString(), + ), + ]); + + runPostRunCallbacks(postRunCallbacks); + } + } + + @override + Future + handleAnalysisHandleWatchEvents( + analyzer_plugin.AnalysisHandleWatchEventsParams parameters, + ) async { + final contextCollection = await _contextCollection.safeFirst; + + for (final event in parameters.events) { + switch (event.type) { + case analyzer_plugin.WatchEventType.REMOVE: + + /// The file was deleted. Let's release associated resources. + final analysisContext = contextCollection.contextFor(event.path); + final key = _AnalysisErrorsKey( + filePath: event.path, + analysisContext: analysisContext, + ); + + _analysisErrorsForAnalysisContexts.remove(key); + default: + // Ignore unhandled watch event types. + break; + } + } + + return super.handleAnalysisHandleWatchEvents(parameters); + } + + @override + Future analyzeFiles({ + required AnalysisContext analysisContext, + required List paths, + }) { + // analyzeFiles reanalyzes all files even if nothing changed by default. + // We customize the behavior to optimize analysis to be performed only + // if something changed + if (paths.isEmpty) return Future.value(); + + return super.analyzeFiles( + analysisContext: analysisContext, + paths: paths, + ); + } + + bool _ownsPath(String path) { + for (final contextRoot + in _client._contextRootsForPlugin.values.expand((e) => e)) { + if (isWithin(contextRoot.root.path, path)) { + final isExcluded = contextRoot.excludedPaths + .any((excludedPath) => isWithin(excludedPath, path)); + if (!isExcluded) return true; + } + } + return false; + } + + @override + Future analyzeFile({ + required AnalysisContext analysisContext, + required String path, + }) async { + if (!analysisContext.contextRoot.isAnalyzed(path)) { + return; + } + + /// analyzeFile might be invoked with an analysisContext that's not + /// part of the enabled context roots. So we separately check that `path` + /// is something we want to analyze + if (!_ownsPath(path)) return; + + final configs = _customLintConfigsForAnalysisContexts[analysisContext]; + if (configs == null) return; + + var ignoreForFiles = []; + if (path.endsWith('.dart')) { + final source = FileSource(resourceProvider.getFile(path)); + if (!source.exists()) return; + ignoreForFiles = parseIgnoreForFile(source.contents.data); + // Lints are disabled for the entire file, so no point in executing lints + if (ignoreForFiles.any((e) => e.disablesAllLints)) return; + } + + final activeLintRules = configs.rules + .where( + (lintRule) => lintRule.filesToAnalyze.any( + (glob) => Glob( + glob, + context: Context( + style: resourceProvider.pathContext.style, + // workaround to: https://github.com/dart-lang/glob/issues/72 + current: analysisContext.contextRoot.root.path, + ), + ).matches(path), + ), + ) + // Removing lints disabled for the file. No need to call LintRule.run + // if they are going to immediately get ignored + .where( + (rule) => !ignoreForFiles.any( + (ignore) => ignore.isIgnored(rule.code.name), + ), + ) + .toList(); + + // Even if the list of lints is empty, we keep going for dart files. Because + // the analyzed file might have some expect-lints comments + if (activeLintRules.isEmpty && !path.endsWith('.dart')) { + // The file is guaranteed to have no analysis error. Therefore we + // abort early to avoid sending a pointless notification + return; + } + + final file = resourceProvider.getFile(path); + final resolver = analysisContext.createResolverForFile(file); + if (resolver == null) return; + + final lintsBeforeExpectLint = []; + final reporterBeforeExpectLint = ErrorReporter( + // TODO assert that a LintRule only emits lints with a code matching LintRule.code + // TODO asserts lintRules can only emit lints in the analyzed file + _AnalysisErrorListenerDelegate((analysisError) async { + final ignoreForLine = + parseIgnoreForLine(analysisError.offset, resolver); + + if (!ignoreForLine.isIgnored(analysisError.errorCode.name)) { + lintsBeforeExpectLint.add(analysisError); + } + }), + resolver.source, + ); + + // TODO: cancel pending analysis if a new analysis is requested on the same file + await _runOperation(() async { + final postRunCallbacks = []; + // TODO implement verbose mode to log lint duration + final registry = NodeLintRegistry(LintRegistry(), enableTiming: false); + final sharedState = {}; + + await Future.wait([ + for (final lintRule in activeLintRules) + _startUpLintRule( + lintRule, + resolver, + reporterBeforeExpectLint, + CustomLintContext( + LintRuleNodeRegistry(registry, lintRule.code.name), + postRunCallbacks.add, + sharedState, + configs.pubspec, + ), + ), + ]); + + await Future.wait([ + for (final lintRule in activeLintRules) + _runLintRule( + lintRule, + resolver, + reporterBeforeExpectLint, + CustomLintContext( + LintRuleNodeRegistry(registry, lintRule.code.name), + postRunCallbacks.add, + sharedState, + configs.pubspec, + ), + ), + ]); + + runPostRunCallbacks(postRunCallbacks); + + final allAnalysisErrors = []; + final analyzerPluginReporter = ErrorReporter( + // TODO assert that a LintRule only emits lints with a code matching LintRule.code + // TODO asserts lintRules can only emit lints in the analyzed file + _AnalysisErrorListenerDelegate(allAnalysisErrors.add), + resolver.source, + ); + + ExpectLint(lintsBeforeExpectLint).run(resolver, analyzerPluginReporter); + + if (await _applyFixes(allAnalysisErrors, resolver, configs, path: path)) { + // Applying fixes re-runs analysis, so lints should've already been sent. + return; + } + + final key = _AnalysisErrorsKey( + filePath: path, + analysisContext: analysisContext, + ); + _analysisErrorsForAnalysisContexts[key] = allAnalysisErrors; + + _channel.sendEvent( + CustomLintEvent.analyzerPluginNotification( + analyzer_plugin.AnalysisErrorsParams( + path, + CustomAnalyzerConverter().convertAnalysisErrors( + allAnalysisErrors, + lineInfo: resolver.lineInfo, + options: analysisContext.getAnalysisOptionsForFile(file), + configSeverities: configs.configs.errors, + ), + ).toNotification(), + ), + ); + }); + } + + Future _applyFixes( + List allAnalysisErrors, + CustomLintResolver resolver, + _CustomLintAnalysisConfigs configs, { + required String path, + }) async { + // The list of already fixed codes, to avoid trying to re-fix a lint + // that should've been fixed before. + final fixedCodes = + (Zone.current[#_fixedCodes] as Set?) ?? {}; + + final context = await _fileContext(path); + if (context == null) return false; + + final allFixes = await _computeFistBatchFixes( + allAnalysisErrors, + context, + fixedCodes, + path: path, + ); + + if (allFixes.isEmpty) return false; + + final source = resolver.source.contents.data; + + try { + final editedSource = analyzer_plugin.SourceEdit.applySequence( + source, + allFixes + .expand((e) => e.fixes) + .expand((e) => e.change.edits) + .expand((e) => e.edits), + ); + + io.File(path).writeAsStringSync(editedSource); + + // Update in-memory file content before re-running analysis. + resourceProvider.setOverlay( + path, + content: editedSource, + modificationStamp: DateTime.now().millisecondsSinceEpoch, + ); + + // Re-run analysis to recompute lints + return runZoned( + () async { + await contentChanged([path]); + return true; + }, + zoneValues: { + // We update the list of fixed codes to avoid re-fixing the same lint + #_fixedCodes: {...fixedCodes, ...allFixes.map((e) => e.error.code)}, + }, + ); + } catch (e) { + // Something failed. We report the original lints + io.stderr.writeln('Failed to apply fixes for $path.\n$e'); + return false; + } + } + + Future> _computeFistBatchFixes( + List allAnalysisErrors, + _FileContext context, + Set fixedCodes, { + required String path, + }) async { + if (!_client.fix) return []; + + final errorToFix = allAnalysisErrors + .where((e) => !fixedCodes.contains(e.errorCode.name)) + .firstOrNull; + if (errorToFix == null) return []; + + final fixes = await _computeFixes( + [errorToFix], + context, + allAnalysisErrors, + ); + + return fixes.map((e) => e.batchFixes ?? e.fix).toList(); + } + + /// Queue an operation to be awaited by [_awaitAnalysisDone] + Future _runOperation(FutureOr Function() cb) async { + final future = Future(cb); + _pendingOperations.add(future); + + try { + return await future; + } finally { + _pendingOperations.remove(future); + } + } + + Future _awaitAnalysisDone({required bool reload}) async { + /// First, we wait for the plugin to be initialized. Otherwise there's + /// obviously no pending operation + final contextCollection = await _contextCollection.safeFirst; + if (reload) { + await afterNewContextCollection(contextCollection: contextCollection); + } + while (_pendingOperations.isNotEmpty) { + await Future.wait([..._pendingOperations]); + } + } + + Future _runLintZoned( + CustomLintResolver resolver, + FutureOr Function() cb, { + ErrorReporter? reporter, + required String name, + }) { + void onLog(String message) { + _channel.sendEvent( + CustomLintEvent.print(message, pluginName: name), + ); + } + + void onError(Object error, StackTrace stackTrace) { + _handleGetLintsError( + resolver, + error, + stackTrace, + reporter: reporter, + pluginName: name, + ); + } + + return asyncRunZonedGuarded( + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => onLog(line), + ), + cb, + onError, + ); + } + + Future _startUpLintRule( + LintRule lintRule, + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext lintContext, + ) async { + await _runLintZoned( + resolver, + () => lintRule.startUp(resolver, lintContext), + reporter: reporter, + name: lintRule.code.name, + ); + } + + Future _runLintRule( + LintRule lintRule, + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext lintContext, + ) async { + await _runLintZoned( + resolver, + () => lintRule.run(resolver, reporter, lintContext), + reporter: reporter, + name: lintRule.code.name, + ); + } + + /// Re-maps uncaught errors by [LintRule] and, if in the IDE, + /// shows a synthetic lint at the top of the file corresponding to the error. + void _handleGetLintsError( + CustomLintResolver resolver, + Object error, + StackTrace stackTrace, { + ErrorReporter? reporter, + required String pluginName, + }) { + _channel.sendEvent( + CustomLintEvent.error( + 'Plugin $pluginName threw while analyzing ${resolver.path}:\n$error', + stackTrace.toString(), + pluginName: pluginName, + ), + ); + + if (reporter == null || !_client.includeBuiltInLints) return; + + const code = LintCode( + name: 'custom_lint_get_lint_fail', + problemMessage: 'A lint plugin threw an exception', + errorSeverity: ErrorSeverity.ERROR, + ); + + // TODO add context message that points to the fir line of the stacktrace + // This involves knowing where a package points to, as a file path is needed + + final startOffset = resolver.lineInfo.lineStarts.firstOrNull ?? 0; + final endOffset = + resolver.lineInfo.lineStarts.elementAtOrNull(1) ?? startOffset; + + reporter.atOffset( + errorCode: code, + offset: startOffset, + length: endOffset - startOffset, + ); + } + + @override + Future handlePluginShutdown( + analyzer_plugin.PluginShutdownParams parameters, + ) async { + await _client._handlePluginShutdown(); + return super.handlePluginShutdown(parameters); + } +} + +class _AnalysisErrorListenerDelegate implements AnalysisErrorListener { + _AnalysisErrorListenerDelegate(this._onError); + + final void Function(AnalysisError error) _onError; + + @override + void onError(AnalysisError error) => _onError(error); +} + +extension on ChangeReporterBuilder { + Future completeAsFixes( + AnalysisError analysisError, + _FileContext context, + ) async { + return analyzer_plugin.AnalysisErrorFixes( + CustomAnalyzerConverter().convertAnalysisError( + analysisError, + lineInfo: context.resolver.lineInfo, + severity: analysisError.errorCode.errorSeverity, + ), + fixes: await complete(), + ); + } +} diff --git a/packages/custom_lint_builder/lib/src/custom_analyzer_converter.dart b/packages/custom_lint_builder/lib/src/custom_analyzer_converter.dart new file mode 100644 index 00000000..5f609eff --- /dev/null +++ b/packages/custom_lint_builder/lib/src/custom_analyzer_converter.dart @@ -0,0 +1,422 @@ +// ignore_for_file: type=lint +// Forked from analyzer_plugin to fix https://github.com/invertase/dart_custom_lint/issues/87 + +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/element/element.dart' as analyzer; +import 'package:analyzer/source/line_info.dart' as analyzer; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/source/source_range.dart' as analyzer; +import 'package:analyzer/diagnostic/diagnostic.dart' as analyzer; +import 'package:analyzer/error/error.dart' as analyzer; +import 'package:analyzer/source/error_processor.dart' as analyzer; +import 'package:analyzer/src/generated/engine.dart' as analyzer; +import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; + +/// An object used to convert between objects defined by the 'analyzer' package +/// and those defined by the plugin protocol. +/// +/// Clients may not extend, implement or mix-in this class. +class CustomAnalyzerConverter { + /// Convert the analysis [error] from the 'analyzer' package to an analysis + /// error defined by the plugin API. If a [lineInfo] is provided then the + /// error's location will have a start line and start column. If a [severity] + /// is provided, then it will override the severity defined by the error. + plugin.AnalysisError convertAnalysisError( + analyzer.AnalysisError error, { + analyzer.LineInfo? lineInfo, + analyzer.ErrorSeverity? severity, + }) { + var errorCode = error.errorCode; + severity ??= errorCode.errorSeverity; + var offset = error.offset; + var startLine = -1; + var startColumn = -1; + var endLine = -1; + var endColumn = -1; + if (lineInfo != null) { + var startLocation = lineInfo.getLocation(offset); + startLine = startLocation.lineNumber; + startColumn = startLocation.columnNumber; + var endLocation = lineInfo.getLocation(offset + error.length); + endLine = endLocation.lineNumber; + endColumn = endLocation.columnNumber; + } + List? contextMessages; + if (error.contextMessages.isNotEmpty) { + contextMessages = error.contextMessages + .map((message) => + convertDiagnosticMessage(message, lineInfo: lineInfo)) + .toList(); + } + return plugin.AnalysisError( + convertErrorSeverity(severity), + convertErrorType(errorCode.type), + plugin.Location( + error.source.fullName, offset, error.length, startLine, startColumn, + endLine: endLine, endColumn: endColumn), + error.message, + errorCode.name.toLowerCase(), + contextMessages: contextMessages, + correction: error.correction, + hasFix: null, + url: errorCode.url, + ); + } + + /// Convert the list of analysis [errors] from the 'analyzer' package to a + /// list of analysis errors defined by the plugin API. If a [lineInfo] is + /// provided then the resulting errors locations will have a start line and + /// start column. If an analysis [options] is provided then the severities of + /// the errors will be altered based on those options. + List convertAnalysisErrors( + List errors, { + analyzer.LineInfo? lineInfo, + analyzer.AnalysisOptions? options, + Map? configSeverities, + }) { + var serverErrors = []; + for (var error in errors) { + var processor = analyzer.ErrorProcessor.getProcessor(options, error); + // Check if there's a severity override in the configs + final configSeverity = configSeverities?[error.errorCode.name]; + + if (processor != null) { + var severity = processor.severity; + // Errors with null severity are filtered out. + if (severity != null) { + // Config severities override processor severities + serverErrors.add(convertAnalysisError( + error, + lineInfo: lineInfo, + severity: configSeverity ?? severity, + )); + } + } else { + // If no processor, still check for config severities + serverErrors.add(convertAnalysisError( + error, + lineInfo: lineInfo, + severity: configSeverity, + )); + } + } + return serverErrors; + } + + /// Convert the diagnostic [message] from the 'analyzer' package to an + /// analysis error defined by the plugin API. If a [lineInfo] is provided then + /// the error's location will have a start line and start column. + plugin.DiagnosticMessage convertDiagnosticMessage( + analyzer.DiagnosticMessage message, + {analyzer.LineInfo? lineInfo}) { + var file = message.filePath; + var offset = message.offset; + var length = message.length; + var startLine = -1; + var startColumn = -1; + var endLine = -1; + var endColumn = -1; + if (lineInfo != null) { + var lineLocation = lineInfo.getLocation(offset); + startLine = lineLocation.lineNumber; + startColumn = lineLocation.columnNumber; + var endLocation = lineInfo.getLocation(offset + length); + endLine = endLocation.lineNumber; + endColumn = endLocation.columnNumber; + } + return plugin.DiagnosticMessage( + message.messageText(includeUrl: true), + plugin.Location(file, offset, length, startLine, startColumn, + endLine: endLine, endColumn: endColumn)); + } + + /// Convert the given [element] from the 'analyzer' package to an element + /// defined by the plugin API. + plugin.Element convertElement(analyzer.Element element) { + var kind = _convertElementToElementKind(element); + return plugin.Element( + kind, + element.displayName, + plugin.Element.makeFlags( + isPrivate: element.isPrivate, + isDeprecated: element.hasDeprecated, + isAbstract: _isAbstract(element), + isConst: _isConst(element), + isFinal: _isFinal(element), + isStatic: _isStatic(element), + ), + location: locationFromElement(element), + typeParameters: _getTypeParametersString(element), + aliasedType: _getAliasedTypeString(element), + parameters: _getParametersString(element), + returnType: _getReturnTypeString(element), + ); + } + + /// Convert the element [kind] from the 'analyzer' package to an element kind + /// defined by the plugin API. + /// + /// This method does not take into account that an instance of [ClassElement] + /// can be an enum and an instance of [FieldElement] can be an enum constant. + /// Use [_convertElementToElementKind] where possible. + plugin.ElementKind convertElementKind(analyzer.ElementKind kind) { + if (kind == analyzer.ElementKind.CLASS) { + return plugin.ElementKind.CLASS; + } else if (kind == analyzer.ElementKind.COMPILATION_UNIT) { + return plugin.ElementKind.COMPILATION_UNIT; + } else if (kind == analyzer.ElementKind.CONSTRUCTOR) { + return plugin.ElementKind.CONSTRUCTOR; + } else if (kind == analyzer.ElementKind.FIELD) { + return plugin.ElementKind.FIELD; + } else if (kind == analyzer.ElementKind.FUNCTION) { + return plugin.ElementKind.FUNCTION; + } else if (kind == analyzer.ElementKind.FUNCTION_TYPE_ALIAS) { + return plugin.ElementKind.FUNCTION_TYPE_ALIAS; + } else if (kind == analyzer.ElementKind.GENERIC_FUNCTION_TYPE) { + return plugin.ElementKind.FUNCTION_TYPE_ALIAS; + } else if (kind == analyzer.ElementKind.GETTER) { + return plugin.ElementKind.GETTER; + } else if (kind == analyzer.ElementKind.LABEL) { + return plugin.ElementKind.LABEL; + } else if (kind == analyzer.ElementKind.LIBRARY) { + return plugin.ElementKind.LIBRARY; + } else if (kind == analyzer.ElementKind.LOCAL_VARIABLE) { + return plugin.ElementKind.LOCAL_VARIABLE; + } else if (kind == analyzer.ElementKind.METHOD) { + return plugin.ElementKind.METHOD; + } else if (kind == analyzer.ElementKind.PARAMETER) { + return plugin.ElementKind.PARAMETER; + } else if (kind == analyzer.ElementKind.PREFIX) { + return plugin.ElementKind.PREFIX; + } else if (kind == analyzer.ElementKind.SETTER) { + return plugin.ElementKind.SETTER; + } else if (kind == analyzer.ElementKind.TOP_LEVEL_VARIABLE) { + return plugin.ElementKind.TOP_LEVEL_VARIABLE; + } else if (kind == analyzer.ElementKind.TYPE_ALIAS) { + return plugin.ElementKind.TYPE_ALIAS; + } else if (kind == analyzer.ElementKind.TYPE_PARAMETER) { + return plugin.ElementKind.TYPE_PARAMETER; + } + return plugin.ElementKind.UNKNOWN; + } + + /// Convert the error [severity] from the 'analyzer' package to an analysis + /// error severity defined by the plugin API. + plugin.AnalysisErrorSeverity convertErrorSeverity( + analyzer.ErrorSeverity severity) => + plugin.AnalysisErrorSeverity.values.byName(severity.name); + + /// Convert the error [type] from the 'analyzer' package to an analysis error + /// type defined by the plugin API. + plugin.AnalysisErrorType convertErrorType(analyzer.ErrorType type) => + plugin.AnalysisErrorType.values.byName(type.name); + + /// Create a location based on an the given [element]. + plugin.Location? locationFromElement(analyzer.Element? element, + {int? offset, int? length}) { + if (element == null || element.source == null) { + return null; + } + offset ??= element.nameOffset; + length ??= element.nameLength; + if (element is analyzer.CompilationUnitElement || + (element is analyzer.LibraryElement && offset < 0)) { + offset = 0; + length = 0; + } + var unitElement = _getUnitElement(element); + var range = analyzer.SourceRange(offset, length); + return _locationForArgs(unitElement, range); + } + + /// Convert the element kind of the [element] from the 'analyzer' package to + /// an element kind defined by the plugin API. + plugin.ElementKind _convertElementToElementKind(analyzer.Element element) { + if (element is analyzer.EnumElement) { + return plugin.ElementKind.ENUM; + } else if (element is analyzer.FieldElement && element.isEnumConstant + // MyEnum.values and MyEnum.one.index return isEnumConstant = true + // so these additional checks are necessary. + // so should it return isEnumConstant = true? + // MyEnum.one.index is final but *not* constant + // so should it return isEnumConstant = true? + // Or should we return ElementKind.ENUM_CONSTANT here + // in either or both of these cases? + ) { + final type = element.type; + if (type is InterfaceType && type.element3 == element.enclosingElement3) { + return plugin.ElementKind.ENUM_CONSTANT; + } + } + return convertElementKind(element.kind); + } + + String? _getAliasedTypeString(analyzer.Element element) { + if (element is analyzer.TypeAliasElement) { + var aliasedType = element.aliasedType; + return aliasedType.getDisplayString(); + } + return null; + } + + /// Return a textual representation of the parameters of the given [element], + /// or `null` if the element does not have any parameters. + String? _getParametersString(analyzer.Element element) { + List parameters; + if (element is analyzer.ExecutableElement) { + // valid getters don't have parameters + if (element.kind == analyzer.ElementKind.GETTER && + element.parameters.isEmpty) { + return null; + } + parameters = element.parameters; + } else if (element is analyzer.TypeAliasElement) { + var aliasedElement = element.aliasedElement; + if (aliasedElement is analyzer.GenericFunctionTypeElement) { + parameters = aliasedElement.parameters; + } else { + return null; + } + } else { + return null; + } + var buffer = StringBuffer(); + var closeOptionalString = ''; + buffer.write('('); + for (var i = 0; i < parameters.length; i++) { + var parameter = parameters[i]; + if (i > 0) { + buffer.write(', '); + } + if (closeOptionalString.isEmpty) { + if (parameter.isNamed) { + buffer.write('{'); + closeOptionalString = '}'; + } else if (parameter.isOptionalPositional) { + buffer.write('['); + closeOptionalString = ']'; + } + } + parameter.appendToWithoutDelimiters(buffer); + } + buffer.write(closeOptionalString); + buffer.write(')'); + return buffer.toString(); + } + + /// Return a textual representation of the return type of the given [element], + /// or `null` if the element does not have a return type. + String? _getReturnTypeString(analyzer.Element element) { + if (element is analyzer.ExecutableElement) { + if (element.kind == analyzer.ElementKind.SETTER) { + return null; + } + return element.returnType.getDisplayString(); + } else if (element is analyzer.VariableElement) { + return element.type.getDisplayString(); + } else if (element is analyzer.TypeAliasElement) { + var aliasedType = element.aliasedType; + if (aliasedType is FunctionType) { + var returnType = aliasedType.returnType; + return returnType.getDisplayString(); + } + } + return null; + } + + /// Return a textual representation of the type parameters of the given + /// [element], or `null` if the element does not have type parameters. + String? _getTypeParametersString(analyzer.Element element) { + if (element is analyzer.TypeParameterizedElement) { + var typeParameters = element.typeParameters; + if (typeParameters.isEmpty) { + return null; + } + return '<${typeParameters.join(', ')}>'; + } + return null; + } + + /// Return the compilation unit containing the given [element]. + analyzer.CompilationUnitElement? _getUnitElement(analyzer.Element element) { + analyzer.Element? currentElement = element; + if (currentElement is analyzer.CompilationUnitElement) { + return currentElement; + } + if (currentElement.enclosingElement3 is analyzer.LibraryElement) { + currentElement = currentElement.enclosingElement3; + } + if (currentElement is analyzer.LibraryElement) { + return currentElement.definingCompilationUnit; + } + for (; + currentElement != null; + currentElement = currentElement.enclosingElement3) { + if (currentElement is analyzer.CompilationUnitElement) { + return currentElement; + } + } + return null; + } + + bool _isAbstract(analyzer.Element element) { + if (element is analyzer.ClassElement) { + return element.isAbstract; + } else if (element is analyzer.MethodElement) { + return element.isAbstract; + } else if (element is analyzer.PropertyAccessorElement) { + return element.isAbstract; + } + return false; + } + + bool _isConst(analyzer.Element element) { + if (element is analyzer.ConstructorElement) { + return element.isConst; + } else if (element is analyzer.VariableElement) { + return element.isConst; + } + return false; + } + + bool _isFinal(analyzer.Element element) { + if (element is analyzer.VariableElement) { + return element.isFinal; + } + return false; + } + + bool _isStatic(analyzer.Element element) { + if (element is analyzer.ExecutableElement) { + return element.isStatic; + } else if (element is analyzer.PropertyInducingElement) { + return element.isStatic; + } + return false; + } + + /// Create and return a location within the given [unitElement] at the given + /// [range]. + plugin.Location? _locationForArgs( + analyzer.CompilationUnitElement? unitElement, + analyzer.SourceRange range) { + if (unitElement == null) { + return null; + } + + var lineInfo = unitElement.lineInfo; + var offsetLocation = lineInfo.getLocation(range.offset); + var endLocation = lineInfo.getLocation(range.offset + range.length); + var startLine = offsetLocation.lineNumber; + var startColumn = offsetLocation.columnNumber; + var endLine = endLocation.lineNumber; + var endColumn = endLocation.columnNumber; + + return plugin.Location(unitElement.source.fullName, range.offset, + range.length, startLine, startColumn, + endLine: endLine, endColumn: endColumn); + } +} diff --git a/packages/custom_lint_builder/lib/src/expect_lint.dart b/packages/custom_lint_builder/lib/src/expect_lint.dart new file mode 100644 index 00000000..f110f371 --- /dev/null +++ b/packages/custom_lint_builder/lib/src/expect_lint.dart @@ -0,0 +1,155 @@ +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:meta/meta.dart'; + +import '../custom_lint_builder.dart'; + +final _expectLintRegex = RegExp(r'//\s*expect_lint\s*:(.+)$', multiLine: true); + +/// A class implementing the logic for `// expect_lint: code` comments +@internal +class ExpectLint { + /// A class implementing the logic for `// expect_lint: code` comments + const ExpectLint(this.analysisErrors); + + static const _code = LintCode( + name: 'unfulfilled_expect_lint', + problemMessage: + 'Expected to find the lint {0} on next line but none found.', + correctionMessage: 'Either update the code such that it emits the lint {0} ' + 'or update the expect_lint clause to not include the code {0}.', + errorSeverity: ErrorSeverity.ERROR, + ); + + /// The list of lints emitted in the file. + final List analysisErrors; + + /// Emits expect_lints + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + ) { + final expectLints = _getAllExpectedLints( + resolver.source.contents.data, + resolver.lineInfo, + filePath: resolver.path, + ); + + final allExpectedLints = expectLints + .map((e) => _ComparableExpectLintMeta(e.line, e.code)) + .toSet(); + + // The list of all the expect_lints codes that don't have a matching lint. + final unfulfilledExpectedLints = expectLints.toList(); + + for (final lint in analysisErrors) { + final lintLine = resolver.lineInfo.getLocation(lint.offset); + + final matchingExpectLintMeta = _ComparableExpectLintMeta( + // Lints use 1-based offsets but expectLints use 0-based offsets. So + // we remove 1 to have them on the same unit. Then we remove 1 again + // to access the line before the lint. + lintLine.lineNumber - 2, + lint.errorCode.name, + ); + + if (allExpectedLints.contains(matchingExpectLintMeta)) { + // The lint has a matching expect_lint. Let's ignore the lint and mark + // the associated expect_lint as fulfilled. + unfulfilledExpectedLints.removeWhere( + (e) => + e.line == matchingExpectLintMeta.line && + e.code == matchingExpectLintMeta.code, + ); + } else { + // The lint has no matching expect_lint. Therefore we let it propagate + reporter.reportError(lint); + } + } + + // Some expect_lint clauses where not respected + for (final unfulfilledExpectedLint in unfulfilledExpectedLints) { + reporter.atOffset( + errorCode: _code, + offset: unfulfilledExpectedLint.offset, + length: unfulfilledExpectedLint.code.length, + arguments: [unfulfilledExpectedLint.code], + ); + } + } + + List<_ExpectLintMeta> _getAllExpectedLints( + String source, + LineInfo lineInfo, { + required String filePath, + }) { + // expect_lint is only supported in dart files as it relies on dart comments + if (!filePath.endsWith('.dart')) return const []; + + final expectLints = _expectLintRegex.allMatches(source); + + return expectLints.expand((expectLint) { + final lineNumber = lineInfo.getLocation(expectLint.start).lineNumber - 1; + final codesStartOffset = source.indexOf(':', expectLint.start) + 1; + + final codes = expectLint.group(1)!.split(','); + var codeOffsetAcc = codesStartOffset; + + return codes.map((rawCode) { + final codeOffset = + codeOffsetAcc + (rawCode.length - rawCode.trimLeft().length); + codeOffsetAcc += rawCode.length + 1; + + final code = rawCode.trim(); + + return _ExpectLintMeta( + line: lineNumber, + code: code, + offset: codeOffset, + ); + }); + }).toList(); + } +} + +/// Information about an `// expect_lint: code` clause +@immutable +class _ExpectLintMeta { + /// Information about an `// expect_lint: code` clause + const _ExpectLintMeta({ + required this.line, + required this.code, + required this.offset, + }) : assert(line >= 0, 'line must be positive'); + + /// A 0-based offset of the line having the expect_lint clause. + final int line; + + /// The code expected. + final String code; + + /// The index of the first character of [code] within the analyzed file. + final int offset; +} + +@immutable +class _ComparableExpectLintMeta { + const _ComparableExpectLintMeta(this.line, this.code); + + final int line; + final String code; + + @override + int get hashCode => Object.hash(line, code); + + @override + bool operator ==(Object other) { + return other is _ComparableExpectLintMeta && + other.code == code && + other.line == line; + } +} diff --git a/packages/custom_lint_builder/lib/src/ignore.dart b/packages/custom_lint_builder/lib/src/ignore.dart new file mode 100644 index 00000000..b5d57b25 --- /dev/null +++ b/packages/custom_lint_builder/lib/src/ignore.dart @@ -0,0 +1,171 @@ +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:collection/collection.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; + +/// Metadata about ignores at a given line. +class IgnoreMetadata { + IgnoreMetadata._( + this._codes, { + required this.startOffset, + required this.endOffset, + }); + + factory IgnoreMetadata._parse( + RegExpMatch? ignore, { + required int startOffset, + }) { + if (ignore == null) return IgnoreMetadata.empty; + + final fullMatchString = ignore.group(0)!; + + final codes = fullMatchString + .substring(fullMatchString.indexOf(':') + 1) + .split(',') + .map((e) => e.trim()) + .toSet(); + + return IgnoreMetadata._( + codes, + startOffset: ignore.start + startOffset, + endOffset: startOffset + ignore.end, + ); + } + + /// Empty metadata. + static final empty = IgnoreMetadata._( + null, + startOffset: -1, + endOffset: -1, + ); + + final Set? _codes; + + /// Whether there are any ignores for this line. + bool get hasIgnore => _codes != null; + + /// Whether all lints are ignored using `type=lint` + // ignore: use_if_null_to_convert_nulls_to_bools + bool get disablesAllLints => _codes?.contains('type=lint') == true; + + /// The offset of where the ignore starts. + /// + /// Will be -1 if there is no ignore. + final int startOffset; + + /// The offset of where the ignore ends. + final int endOffset; + + /// Whether the given code is ignored. + bool isIgnored(String code) { + final codes = _codes; + if (codes == null) return false; + return codes.contains(code) || disablesAllLints; + } +} + +final _ignoreRegex = RegExp(r'//\s*ignore\s*:.*?$', multiLine: true); + +/// Searches for `// ignore:` matching a given line. +IgnoreMetadata parseIgnoreForLine( + int offset, + CustomLintResolver resolver, +) { + final line = resolver.lineInfo.getLocation(offset).lineNumber - 1; + + if (line <= 0) return IgnoreMetadata.empty; + + final previousLineOffset = resolver.lineInfo.getOffsetOfLine(line - 1); + final previousLine = resolver.source.contents.data.substring( + previousLineOffset, + offset - 1, + ); + + final codeContent = _ignoreRegex.firstMatch(previousLine); + if (codeContent == null) return IgnoreMetadata.empty; + + return IgnoreMetadata._parse(codeContent, startOffset: previousLineOffset); +} + +final _ignoreForFileRegex = + RegExp(r'//\s*ignore_for_file\s*:.*$', multiLine: true); + +/// Searches for `// ignore_for_file:` in a given file. +List parseIgnoreForFile(String source) { + final ignoreForFiles = _ignoreForFileRegex.allMatches(source).nonNulls; + + return ignoreForFiles + .map((e) => IgnoreMetadata._parse(e, startOffset: 0)) + .toList(); +} + +/// Built in fix to ignore a lint. +class IgnoreCode extends DartFix { + /// The code for 'ignore for line' fix. + static const ignoreId = '<>'; + + @override + String get id => ignoreId; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + final ignoreForLine = parseIgnoreForLine(analysisError.offset, resolver); + final ignoreForFile = parseIgnoreForFile(resolver.source.contents.data); + + final ignoreForLineChangeBuilder = reporter.createChangeBuilder( + message: 'Ignore "${analysisError.errorCode.name}" for line', + priority: 1, + ); + + ignoreForLineChangeBuilder.addDartFileEdit((builder) { + if (ignoreForLine.hasIgnore) { + builder.addSimpleInsertion( + ignoreForLine.endOffset, + ', ${analysisError.errorCode.name}', + ); + } else { + final offsetLine = + resolver.lineInfo.getLocation(analysisError.offset).lineNumber - 1; + + final startLineOffset = resolver.lineInfo.getOffsetOfLine(offsetLine); + + final indentLength = resolver.source.contents.data + .substring(startLineOffset) + .indexOf(RegExp(r'\S')); + + builder.addSimpleInsertion( + startLineOffset, + '${' ' * indentLength}// ignore: ${analysisError.errorCode.name}\n', + ); + } + }); + + final ignoreForFileChangeBuilder = reporter.createChangeBuilder( + message: 'Ignore "${analysisError.errorCode.name}" for file', + priority: 0, + ); + + ignoreForFileChangeBuilder.addDartFileEdit((builder) { + final firstIgnore = ignoreForFile.firstOrNull; + if (firstIgnore == null) { + builder.addSimpleInsertion( + 0, + '// ignore_for_file: ${analysisError.errorCode.name}\n', + ); + } else { + builder.addSimpleInsertion( + firstIgnore.endOffset, + ', ${analysisError.errorCode.name}', + ); + } + }); + } +} diff --git a/packages/custom_lint_builder/lib/src/plugin_base.dart b/packages/custom_lint_builder/lib/src/plugin_base.dart deleted file mode 100644 index 0ed47f53..00000000 --- a/packages/custom_lint_builder/lib/src/plugin_base.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer_plugin/protocol/protocol_common.dart'; -import 'package:analyzer_plugin/protocol/protocol_generated.dart'; - -abstract class PluginBase { - Iterable getLints(LibraryElement library); - - Iterable getFixes(LibraryElement library, int offset) { - return const []; - } -} diff --git a/packages/custom_lint_builder/lib/src/pragrams.dart b/packages/custom_lint_builder/lib/src/pragrams.dart new file mode 100644 index 00000000..bf7a8e0d --- /dev/null +++ b/packages/custom_lint_builder/lib/src/pragrams.dart @@ -0,0 +1,5 @@ +import 'package:meta/meta.dart'; + +/// Alias for vm:prefer-inline +@internal +const preferInline = pragma('vm:prefer-inline'); diff --git a/packages/custom_lint_builder/pubspec.lock b/packages/custom_lint_builder/pubspec.lock deleted file mode 100644 index 08f47069..00000000 --- a/packages/custom_lint_builder/pubspec.lock +++ /dev/null @@ -1,201 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "36.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1" - analyzer_plugin: - dependency: "direct main" - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.0" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - custom_lint: - dependency: "direct main" - description: - path: "../custom_lint" - relative: true - source: path - version: "0.0.1" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.2" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - package_config: - dependency: "direct main" - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: "direct main" - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - pub_semver: - dependency: "direct main" - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - pubspec_parse: - dependency: "direct main" - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - recase: - dependency: "direct main" - description: - name: recase - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - uuid: - dependency: transitive - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" -sdks: - dart: ">=2.16.0 <3.0.0" diff --git a/packages/custom_lint_builder/pubspec.yaml b/packages/custom_lint_builder/pubspec.yaml index 48900297..31ef07a3 100644 --- a/packages/custom_lint_builder/pubspec.yaml +++ b/packages/custom_lint_builder/pubspec.yaml @@ -1,17 +1,30 @@ name: custom_lint_builder -version: 0.0.1 -publish_to: none +version: 0.7.5 +description: A package to help writing custom linters +repository: https://github.com/invertase/dart_custom_lint environment: - sdk: ">=2.16.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - analyzer: ^3.3.1 - analyzer_plugin: ^0.9.0 - custom_lint: - path: ../custom_lint - package_config: ^2.0.2 - path: ^1.8.1 - pub_semver: ^2.1.1 + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + collection: ^1.16.0 + # Using tight constraints as custom_lint_builder communicate with each-other + # using a specific contract + custom_lint: 0.7.5 + # Using tight constraints as custom_lint_builder communicate with each-other + # using a specific contract + custom_lint_core: 0.7.5 + # Using loose constraint to support a range of analyzer versions. + custom_lint_visitor: ^1.0.0 + glob: ^2.1.1 + hotreloader: ">=3.0.5 <5.0.0" + meta: ^1.7.0 + package_config: ^2.1.0 + path: ^1.8.0 pubspec_parse: ^1.2.0 - recase: ^4.0.0 + rxdart: ^0.28.0 + +dev_dependencies: + test: ^1.22.2 diff --git a/packages/custom_lint_builder/pubspec_overrides.yaml b/packages/custom_lint_builder/pubspec_overrides.yaml new file mode 100644 index 00000000..aac3d839 --- /dev/null +++ b/packages/custom_lint_builder/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: custom_lint_visitor +dependency_overrides: + custom_lint: + path: ../custom_lint + custom_lint_core: + path: ../custom_lint_core + lint_visitor_generator: + path: ../lint_visitor_generator diff --git a/packages/custom_lint_builder/test/analyzer_converter_test.dart b/packages/custom_lint_builder/test/analyzer_converter_test.dart new file mode 100644 index 00000000..d39b7b2b --- /dev/null +++ b/packages/custom_lint_builder/test/analyzer_converter_test.dart @@ -0,0 +1,154 @@ +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:analyzer/source/file_source.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:custom_lint_builder/src/custom_analyzer_converter.dart'; +import 'package:test/test.dart'; + +void main() { + test('Converts LintCode', () { + final resourceProvider = MemoryResourceProvider(); + final source = FileSource( + resourceProvider.newFile( + '/home/user/project/lib/main.dart', + 'void main() {}', + ), + ); + final source2 = FileSource( + resourceProvider.newFile( + '/home/user/project/lib/main2.dart', + 'void main2() {}', + ), + ); + + final another = AnalysisError.tmp( + source: source, + offset: 11, + length: 12, + errorCode: const LintCode( + name: 'another', + problemMessage: 'another message', + url: 'https://dart.dev/diagnostics/another', + ), + ); + + expect( + CustomAnalyzerConverter() + .convertAnalysisError( + AnalysisError.tmp( + source: source2, + offset: 13, + length: 14, + errorCode: const LintCode( + name: 'foo', + problemMessage: 'bar', + url: 'https://google.com/diagnostics/foo', + ), + contextMessages: [another.problemMessage], + ), + ) + .toJson(), + { + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '/home/user/project/lib/main2.dart', + 'offset': 13, + 'length': 14, + 'startLine': -1, + 'startColumn': -1, + 'endLine': -1, + 'endColumn': -1, + }, + 'message': 'bar', + 'code': 'foo', + 'url': 'https://google.com/diagnostics/foo', + 'contextMessages': [ + { + 'message': 'another message', + 'location': { + 'file': '/home/user/project/lib/main.dart', + 'offset': 11, + 'length': 12, + 'startLine': -1, + 'startColumn': -1, + 'endLine': -1, + 'endColumn': -1, + }, + } + ], + }, + ); + }); + + test('Respects configSeverities when converting errors', () { + final resourceProvider = MemoryResourceProvider(); + final source = FileSource( + resourceProvider.newFile( + '/home/user/project/lib/main.dart', + 'void main() {}', + ), + ); + + // Create an analysis error with INFO severity + final error = AnalysisError.tmp( + source: source, + offset: 0, + length: 4, + errorCode: const LintCode( + name: 'rule_name_1', + problemMessage: 'This is a lint', + ), + ); + + // Create config severities map that changes rule_name_1 to ERROR + final configSeverities = { + 'rule_name_1': ErrorSeverity.ERROR, + 'rule_name_2': ErrorSeverity.WARNING, + }; + + final converter = CustomAnalyzerConverter(); + + // Convert the error without config severities - should be INFO + final defaultResult = converter.convertAnalysisError(error); + expect(defaultResult.severity.name, 'INFO'); + + // Convert the error with direct severity override + final withSeverityParam = converter.convertAnalysisError( + error, + severity: ErrorSeverity.ERROR, + ); + expect(withSeverityParam.severity.name, 'ERROR'); + + // Convert the error with config severities through convertAnalysisErrors + final withConfigSeverities = converter.convertAnalysisErrors( + [error], + configSeverities: configSeverities, + ).single; + + // Config severities should have overridden the default severity + expect(withConfigSeverities.severity.name, 'ERROR'); + + // Create an error with a rule name that doesn't have a config severity + final errorWithoutConfigSeverity = AnalysisError.tmp( + source: source, + offset: 0, + length: 4, + errorCode: const LintCode( + name: 'no_config_rule', + problemMessage: 'This is another lint', + ), + ); + + final noConfigResult = converter.convertAnalysisErrors( + [errorWithoutConfigSeverity], + configSeverities: configSeverities, + ).single; + + // Should use default severity when not in config + expect(noConfigResult.severity.name, 'INFO'); + }); +} diff --git a/packages/custom_lint_core/CHANGELOG.md b/packages/custom_lint_core/CHANGELOG.md new file mode 100644 index 00000000..de87e73b --- /dev/null +++ b/packages/custom_lint_core/CHANGELOG.md @@ -0,0 +1,162 @@ +## 0.7.2 - 2025-02-27 + +Fix inconsistent version + +## 0.7.1 - 2025-01-08 + +- Support analyzer 7.0.0 + +## 0.7.0 - 2024-10-27 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + +## 0.6.10 - 2024-10-10 + +- Added support for `dart:io` imports when using `TypeChecker.fromPackage` (thanks to @oskar-zeinomahmalat-sonarsource) + +## 0.6.9 - 2024-10-09 + +- Support analyzer 0.6.9 + +## 0.6.5 - 2024-08-15 + +- Upgraded to analyzer ^6.6.0. + This is a quick fix to unblock the stable Flutter channel. + A more robust fix will come later. +- Fixed a bug where isSuperTypeOf throws if the element is null (thanks to @charlescyt) + +## 0.6.3 - 2024-03-16 + +- Parses `debug`/`config` flags + +## 0.6.2 - 2024-02-19 + +- Fix null exception when using `TypeChecker.isSuperTypeOf` (thanks to @charlescyt) + +## 0.6.1 - 2024-02-14 + +- Exported `NodeLintRegistry` + +## 0.6.0 - 2024-02-04 + +- Bumped minimum Dart SDK to 3.0.0 +- goldens with diffs now include the priority, ID, selection and file path. +- **breaking**: `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot`'s `source` + parameter now takes a `Map? sources` instead of `String? source`. + This enables goldens to handle fixes that emit to a different file. + +## 0.5.14 - 2024-02-03 + +- Improved formatting when specifying `source` on `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot` + +## 0.5.13 - 2024-02-03 + +- Improved formatting when specifying `source` on `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot` + +## 0.5.12 - 2024-02-02 + +- Added `encodePrioritizedSourceChanges`, to enable writing a `List` to a file +- `matcherNormalizedPrioritizedSourceChangeSnapshot` now optionally + takes a `String source`. This enables saving to the disk the expected + result. + +## 0.5.11 - 2024-01-27 + +- `custom_lint` upgraded to `0.5.11` + +## 0.5.10 - 2024-01-26 + +- Fix a bug with `matcherNormalizedPrioritizedSourceChangeSnapshot` + +## 0.5.9 - 2024-01-26 + +- `matcherNormalizedPrioritizedSourceChangeSnapshot` now optionally allows specifying a `JsonEncoder`. + +## 0.5.8 - 2024-01-09 + +Added an optional `customPath` to the various `ChangeReporter` methods (thanks to @laurentschall) + +## 0.5.7 - 2023-11-20 + +- `custom_lint` upgraded to `0.5.7` + +## 0.5.6 - 2023-10-30 + +- `custom_lint` upgraded to `0.5.6` + +## 0.5.5 - 2023-10-26 + +- `custom_lint` upgraded to `0.5.5` + +## 0.5.4 - 2023-10-20 + +- `custom_lint` upgraded to `0.5.4` + +## 0.5.3 - 2023-08-29 + +- `custom_lint` upgraded to `0.5.3` + +## 0.5.2 - 2023-08-16 + +- Support both analyzer 5.12.0 and 6.0.0 at the same time. +- Attempt at fixing the windows crash + +## 0.5.1 - 2023-08-03 + +Support analyzer v6 + +## 0.5.0 - 2023-06-21 + +- `custom_lint` upgraded to `0.5.0` + +## 0.4.0 - 2023-05-12 + +- Added support for analyzer 5.12.0 + +## 0.3.4 - 2023-04-19 + +- `custom_lint` upgraded to `0.3.4` + +## 0.3.3 - 2023-04-06 + +- Upgraded `analyzer` to `>=5.7.0 <5.11.0` +- `LintRuleNodeRegistry` and other AstVisitor-like now are based off `GeneralizingAstVisitor` instead of `GeneralizingAstVisitor` +- Exposes the Pubspec in CustomLintContext + +## 0.3.2 - 2023-03-09 + +- `custom_lint` upgraded to `0.3.2` + +## 0.3.1 - 2023-03-09 + +Update dependencies + +## 0.3.0 - 2023-03-09 + +- Update analyzer to >=5.7.0 <5.8.0 + +## 0.2.12 + +Upgrade custom_lint + +## 0.2.11 + +Bump minimum Dart SDK to `sdk: ">=2.19.0 <3.0.0"` + +## 0.2.10 + +Update `fileToAnalyze` from `*.dart` to `**.dart` to match the `fileToAnalyze` fix in `custom_lint_builder` + +## 0.2.9 + +Fix `TypeChecker.fromPackage` not always return `true` when it should + +## 0.2.8 + +Fix exception thrown by `TypeChecker.isExactlyType` if `DartType.element` is `null`. + +## 0.2.7 + +Initial release diff --git a/packages/custom_lint_core/LICENSE b/packages/custom_lint_core/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/packages/custom_lint_core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/custom_lint_core/README.md b/packages/custom_lint_core/README.md new file mode 100644 index 00000000..6a66a3d0 --- /dev/null +++ b/packages/custom_lint_core/README.md @@ -0,0 +1,31 @@ +

+

custom_lint_core

+ An package exposing base classes for defining lint rules/fixes/assists. +

+ +

+ License +

+ +## About + +`custom_lint_core`, a variant of `custom_lint_builder` which exports lint-utilities without +causing custom_lint to consider the dependent as a lint plugin. + +As opposed to `custom_lint_builder` , adding `custom_lint_core` as dependency will not flag +a package as a "custom_lint plugin". + +See [custom_lint] for more informations + +--- + +

+ + + +

+ Built and maintained by Invertase. +

+

+ +[custom_lint]: https://github.com/invertase/dart_custom_lint diff --git a/packages/custom_lint_core/build.yaml b/packages/custom_lint_core/build.yaml new file mode 100644 index 00000000..6cb1b3de --- /dev/null +++ b/packages/custom_lint_core/build.yaml @@ -0,0 +1,12 @@ +targets: + $default: + builders: + lint_visitor_generator: + enabled: true + generate_for: + include: + - "**/node_lint_visitor.dart" + source_gen|combining_builder: + options: + ignore_for_file: + - "type=lint" diff --git a/packages/custom_lint_core/lib/custom_lint_core.dart b/packages/custom_lint_core/lib/custom_lint_core.dart new file mode 100644 index 00000000..deab9b5f --- /dev/null +++ b/packages/custom_lint_core/lib/custom_lint_core.dart @@ -0,0 +1,22 @@ +export 'package:custom_lint_visitor/custom_lint_visitor.dart' + hide LintRegistry, LinterVisitor, NodeLintRegistry; + +export 'src/assist.dart'; +export 'src/change_reporter.dart' + hide + BatchChangeReporterBuilder, + BatchChangeReporterImpl, + ChangeBuilderImpl, + ChangeReporterBuilder, + ChangeReporterBuilderImpl, + ChangeReporterImpl; +export 'src/configs.dart'; +export 'src/fixes.dart' hide FixArgs; +export 'src/lint_codes.dart'; +export 'src/lint_rule.dart'; +export 'src/matcher.dart'; +export 'src/package_utils.dart' hide FindProjectError; +export 'src/plugin_base.dart' hide runPostRunCallbacks; +export 'src/resolver.dart' hide CustomLintResolverImpl; +export 'src/source_range_extensions.dart'; +export 'src/type_checker.dart'; diff --git a/packages/custom_lint_core/lib/src/assist.dart b/packages/custom_lint_core/lib/src/assist.dart new file mode 100644 index 00000000..ba96444c --- /dev/null +++ b/packages/custom_lint_core/lib/src/assist.dart @@ -0,0 +1,141 @@ +import 'dart:io' as io; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; +import 'package:meta/meta.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'change_reporter.dart'; +import 'fixes.dart'; +import 'lint_rule.dart'; +import 'plugin_base.dart'; +import 'resolver.dart'; + +/// A base class for assists. +/// +/// Assists are more typically known as "refactoring". They are changes +/// triggered by the user, without an associated problem. As opposed to a [Fix], +/// which represents a source change but is associated with an issue. +/// +/// For creating assists inside Dart files, see [DartAssist]. +/// +/// Suclassing [Assist] can be helpful if you wish to implement assists for +/// non-Dart files (yaml, json, ...) +/// +/// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/assists.md +@immutable +abstract class Assist { + /// A list of glob patterns matching the files that [run] cares about. + /// + /// This can include Dart files, Yaml files, ... + List get filesToAnalyze; + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + SourceRange target, + ) async {} + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ); +} + +/// A base class for creating assists inside Dart files. +/// +/// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/assists.md#Defining-a-dart-assist +@immutable +abstract class DartAssist extends Assist { + static final _stateKey = Object(); + + @override + List get filesToAnalyze => const ['**.dart']; + + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + SourceRange target, + ) async { + // Relying on shared state to execute all linters in a single AstVisitor + if (context.sharedState.containsKey(_stateKey)) return; + context.sharedState[_stateKey] = Object(); + + final unit = await resolver.getResolvedUnitResult(); + + context.addPostRunCallback(() { + final linterVisitor = LinterVisitor(context.registry.nodeLintRegistry); + + unit.unit.accept(linterVisitor); + }); + } + + /// Runs this assist in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + /// + /// The parameter [pubspec] can be used to simulate a pubspec file which will + /// be passed to [CustomLintContext.pubspec]. + /// By default, an empty pubspec with the name `test_project` will be used. + @visibleForTesting + Future> testRun( + ResolvedUnitResult result, + SourceRange target, { + Pubspec? pubspec, + }) async { + final registry = LintRuleNodeRegistry( + NodeLintRegistry(LintRegistry(), enableTiming: false), + 'unknown', + ); + final postRunCallbacks = []; + final context = CustomLintContext( + registry, + postRunCallbacks.add, + {}, + pubspec, + ); + final resolver = CustomLintResolverImpl( + () => Future.value(result), + lineInfo: result.lineInfo, + path: result.path, + source: result.libraryElement.source, + ); + final reporter = ChangeReporterImpl(result.session, resolver); + + await startUp( + resolver, + context, + target, + ); + + run(resolver, reporter, context, target); + runPostRunCallbacks(postRunCallbacks); + + return reporter.complete(); + } + + /// Analyze a Dart file and runs this assist in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + @visibleForTesting + Future> testAnalyzeAndRun( + io.File file, + SourceRange target, + ) async { + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + return testRun(result, target); + } +} diff --git a/packages/custom_lint_core/lib/src/change_reporter.dart b/packages/custom_lint_core/lib/src/change_reporter.dart new file mode 100644 index 00000000..33c0b348 --- /dev/null +++ b/packages/custom_lint_core/lib/src/change_reporter.dart @@ -0,0 +1,303 @@ +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart' + as analyzer_plugin; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_yaml.dart'; +import 'package:meta/meta.dart'; + +import 'resolver.dart'; + +/// A class used for requesting a [ChangeBuilder]. +abstract class ChangeReporter { + /// Creates a [ChangeBuilder], which can then be used to modify files. + /// + /// [message] is the name that will show-up in the IDE when users request changes. + /// + /// [priority] defines how high/low in the list of proposed changes will this + /// change be. + ChangeBuilder createChangeBuilder({ + required String message, + required int priority, + }); + + /// Waits for all [ChangeBuilder] to fully compute the source changes. + Future waitForCompletion(); + + /// Waits for completion and obtains the changes. + /// + /// This life-cycle can only be called once per [ChangeReporter]. + Future> complete(); +} + +@internal +abstract class ChangeReporterBuilder { + ChangeReporter createChangeReporter({required String id}); + + Future> complete(); + + Future waitForCompletion(); +} + +@internal +class BatchChangeReporterBuilder extends ChangeReporterBuilder { + BatchChangeReporterBuilder(ChangeBuilderImpl batchBuilder) + : _reporter = BatchChangeReporterImpl(batchBuilder); + + final BatchChangeReporterImpl _reporter; + + @override + ChangeReporter createChangeReporter({required String id}) => _reporter; + + @override + Future waitForCompletion() => _reporter.waitForCompletion(); + + @override + Future> complete() => _reporter.complete(); +} + +@internal +class BatchChangeReporterImpl implements ChangeReporter { + BatchChangeReporterImpl(this.batchBuilder); + + final ChangeBuilderImpl batchBuilder; + + @override + ChangeBuilder createChangeBuilder({ + required String message, + required int priority, + String? id, + }) { + return batchBuilder; + } + + @override + Future waitForCompletion() async => batchBuilder.waitForCompletion(); + + @override + Future> complete() async { + return [await batchBuilder.complete()]; + } +} + +@internal +class ChangeReporterBuilderImpl extends ChangeReporterBuilder { + ChangeReporterBuilderImpl(this._resolver, this._analysisSession); + + final CustomLintResolver _resolver; + final AnalysisSession _analysisSession; + final List _reporters = []; + + @override + ChangeReporter createChangeReporter({required String id}) { + final reporter = ChangeReporterImpl( + _analysisSession, + _resolver, + id: id, + ); + _reporters.add(reporter); + + return reporter; + } + + @override + Future waitForCompletion() async { + await Future.wait( + _reporters.map((e) => e.waitForCompletion()), + ); + } + + @override + Future> complete() async { + final changes = Stream.fromFutures( + _reporters.map((e) => e.complete()), + ); + + return changes.expand((e) => e).toList(); + } +} + +/// The implementation of [ChangeReporter] +@internal +class ChangeReporterImpl implements ChangeReporter { + /// The implementation of [ChangeReporter] + ChangeReporterImpl( + this._analysisSession, + this._resolver, { + this.id, + }); + + final CustomLintResolver _resolver; + final AnalysisSession _analysisSession; + final _changeBuilders = []; + final String? id; + + @override + ChangeBuilderImpl createChangeBuilder({ + required String message, + required int priority, + }) { + final changeBuilderImpl = ChangeBuilderImpl( + message, + analysisSession: _analysisSession, + priority: priority, + id: id, + path: _resolver.path, + ); + _changeBuilders.add(changeBuilderImpl); + + return changeBuilderImpl; + } + + @override + Future waitForCompletion() async { + await Future.wait( + _changeBuilders.map((e) => e.waitForCompletion()), + ); + } + + @override + Future> complete() async { + return Future.wait( + _changeBuilders.map((e) => e.complete()), + ); + } +} + +/// A class for modifying +abstract class ChangeBuilder { + /// Use the [buildFileEdit] function to create a collection of edits to the + /// currently analyzed file. The edits will be added to the source change + /// that is being built. + /// + /// The builder passed to the [buildFileEdit] function has additional support + /// for working with Dart source files. + /// + /// Use the [customPath] if the collection of edits should be written to another + /// dart file. + void addDartFileEdit( + void Function(DartFileEditBuilder builder) buildFileEdit, { + ImportPrefixGenerator importPrefixGenerator, + String? customPath, + }); + + /// Use the [buildFileEdit] function to create a collection of edits to the + /// currently analyzed file. The edits will be added to the source change + /// that is being built. + /// + /// The builder passed to the [buildFileEdit] function has no special support + /// for any particular kind of file. + /// + /// Use the [customPath] if the collection of edits should be written to another + /// file. + void addGenericFileEdit( + void Function(analyzer_plugin.FileEditBuilder builder) buildFileEdit, { + String? customPath, + }); + + /// Use the [buildFileEdit] function to create a collection of edits to the + /// currently analyzed file. The edits will be added to the source change + /// that is being built. + /// + /// The builder passed to the [buildFileEdit] function has additional support + /// for working with YAML source files. + /// + /// Use the [customPath] if the collection of edits should be written to another + /// YAML file. + void addYamlFileEdit( + void Function(YamlFileEditBuilder builder) buildFileEdit, + String? customPath, + ); +} + +@internal +class ChangeBuilderImpl implements ChangeBuilder { + ChangeBuilderImpl( + this._message, { + required this.path, + required this.priority, + required this.id, + required AnalysisSession analysisSession, + }) : _innerChangeBuilder = + analyzer_plugin.ChangeBuilder(session: analysisSession); + + final String _message; + final int priority; + final String path; + final String? id; + final analyzer_plugin.ChangeBuilder _innerChangeBuilder; + var _completed = false; + final _operations = >[]; + + @override + void addDartFileEdit( + void Function(DartFileEditBuilder builder) buildFileEdit, { + ImportPrefixGenerator? importPrefixGenerator, + String? customPath, + }) { + _operations.add( + Future(() async { + return importPrefixGenerator == null + ? _innerChangeBuilder.addDartFileEdit( + customPath ?? path, + buildFileEdit, + ) + : _innerChangeBuilder.addDartFileEdit( + customPath ?? path, + buildFileEdit, + importPrefixGenerator: importPrefixGenerator, + ); + }), + ); + } + + @override + void addGenericFileEdit( + void Function(analyzer_plugin.FileEditBuilder builder) buildFileEdit, { + String? customPath, + }) { + _operations.add( + Future( + () async => _innerChangeBuilder.addGenericFileEdit( + customPath ?? path, + buildFileEdit, + ), + ), + ); + } + + @override + void addYamlFileEdit( + void Function(YamlFileEditBuilder builder) buildFileEdit, + String? customPath, + ) { + _operations.add( + Future( + () async => _innerChangeBuilder.addYamlFileEdit( + customPath ?? path, + buildFileEdit, + ), + ), + ); + } + + Future waitForCompletion() async { + await Future.wait(_operations); + } + + Future complete() async { + if (_completed) { + throw StateError('Cannot call waitForCompletion more than once'); + } + _completed = true; + + await waitForCompletion(); + + return PrioritizedSourceChange( + priority, + _innerChangeBuilder.sourceChange + ..id = id + ..message = _message, + ); + } +} diff --git a/packages/custom_lint_core/lib/src/configs.dart b/packages/custom_lint_core/lib/src/configs.dart new file mode 100644 index 00000000..cfdf26ce --- /dev/null +++ b/packages/custom_lint_core/lib/src/configs.dart @@ -0,0 +1,226 @@ +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart'; +import 'package:yaml/yaml.dart'; + +import '../custom_lint_core.dart'; + +/// Configurations representing the custom_lint metadata in the project's `analysis_options.yaml`. +@immutable +class CustomLintConfigs { + /// Configurations representing the custom_lint metadata in the project's `analysis_options.yaml`. + @internal + const CustomLintConfigs({ + required this.enableAllLintRules, + required this.verbose, + required this.debug, + required this.rules, + required this.errors, + }); + + /// Decode a [CustomLintConfigs] from a file. + factory CustomLintConfigs.parse( + File? analysisOptionsFile, + PackageConfig packageConfig, + ) { + if (analysisOptionsFile == null || !analysisOptionsFile.exists) { + return CustomLintConfigs.empty; + } + + final optionsString = analysisOptionsFile.readAsStringSync(); + Object? yaml; + try { + yaml = loadYaml(optionsString) as Object?; + } catch (err) { + return CustomLintConfigs.empty; + } + if (yaml is! Map) return CustomLintConfigs.empty; + + final include = yaml['include'] as Object?; + var includedOptions = CustomLintConfigs.empty; + if (include is String) { + final includeUri = Uri.parse(include); + String? includeAbsolutePath; + + if (includeUri.scheme == 'package') { + final packageUri = packageConfig.resolve(includeUri); + includeAbsolutePath = packageUri?.toFilePath(); + } else { + includeAbsolutePath = normalize( + absolute( + analysisOptionsFile.parent.path, + includeUri.toFilePath(), + ), + ); + } + + if (includeAbsolutePath != null) { + includedOptions = CustomLintConfigs.parse( + analysisOptionsFile.provider.getFile(includeAbsolutePath), + packageConfig, + ); + } + } + + final customLint = yaml['custom_lint'] as Object?; + if (customLint is! Map) return includedOptions; + + final rules = {...includedOptions.rules}; + final enableAllLintRulesYaml = customLint['enable_all_lint_rules']; + final enableAllLintRules = enableAllLintRulesYaml is bool + ? enableAllLintRulesYaml + : includedOptions.enableAllLintRules; + + final debugYaml = customLint['debug']; + final debug = debugYaml is bool ? debugYaml : includedOptions.debug; + + final verboseYaml = customLint['verbose']; + final verbose = verboseYaml is bool ? verboseYaml : includedOptions.verbose; + + final rulesYaml = customLint['rules'] as Object?; + + if (rulesYaml is List) { + // Supports: + // rules: + // - prefer_lint + // - map: false + // - map2: + // length: 42 + + for (final item in rulesYaml) { + if (item is String) { + rules[item] = const LintOptions.empty(enabled: true); + } else if (item is Map) { + final key = item.keys.first as String; + final value = item.values.first; + final enabled = value is bool? ? value : null; + + rules[key] = LintOptions.fromYaml( + Map.fromEntries( + item.entries + .skip(1) + .map((e) => MapEntry(e.key as String, e.value)), + ), + enabled: enabled ?? true, + ); + } + } + } + + final errors = {...includedOptions.errors}; + + final errorsYaml = customLint['errors'] as Object?; + if (errorsYaml is Map) { + for (final entry in errorsYaml.entries) { + final value = entry.value; + if (entry.key case final String key?) { + final severity = ErrorSeverity.values.firstWhereOrNull( + (e) => e.displayName == value, + ); + if (severity == null) { + throw ArgumentError( + 'Provided error severity: $value specified for key: $key is not valid. ' + 'Valid error severities are: ${ErrorSeverity.values.map((e) => e.displayName).join(', ')}', + ); + } + errors[key] = severity; + } + } + } + + return CustomLintConfigs( + enableAllLintRules: enableAllLintRules, + verbose: verbose, + debug: debug, + rules: UnmodifiableMapView(rules), + errors: UnmodifiableMapView(errors), + ); + } + + /// An empty custom_lint configuration + @internal + static const empty = CustomLintConfigs( + enableAllLintRules: null, + verbose: false, + debug: false, + rules: {}, + errors: {}, + ); + + /// A field representing whether to enable/disable lint rules that are not + /// listed in: + /// + /// ```yaml + /// custom_lint: + /// rules: + /// ... + /// ``` + /// + /// If `null`, the default behavior will be deferred to [LintRule.enabledByDefault]. + final bool? enableAllLintRules; + + /// A list of lints that are explicitly enabled/disabled in the config file, + /// along with extra per-lint configuration. + final Map rules; + + /// Whether to enable verbose logging. + final bool verbose; + + /// Whether enable hot-reload and log the VM-service URI. + final bool debug; + + /// A map of error codes to their severity. + final Map errors; + + @override + bool operator ==(Object other) => + other is CustomLintConfigs && + other.enableAllLintRules == enableAllLintRules && + other.verbose == verbose && + other.debug == debug && + const MapEquality().equals(other.rules, rules) && + const MapEquality().equals(other.errors, errors); + + @override + int get hashCode => Object.hash( + enableAllLintRules, + verbose, + debug, + const MapEquality().hash(rules), + const MapEquality().hash(errors), + ); +} + +/// Option information for a specific [LintRule]. +@immutable +class LintOptions { + /// Creates a [LintOptions] from YAML. + @internal + const LintOptions.fromYaml(Map yaml, {required this.enabled}) + : json = yaml; + + /// Options with no [json] + @internal + const LintOptions.empty({required this.enabled}) : json = const {}; + + /// Whether the configuration enables/disables the lint rule. + final bool enabled; + + /// Extra configurations for a [LintRule]. + final Map json; + + @override + bool operator ==(Object other) => + other is LintOptions && + other.enabled == enabled && + const MapEquality().equals(other.json, json); + + @override + int get hashCode => Object.hash( + enabled, + const MapEquality().hash(json), + ); +} diff --git a/packages/custom_lint_core/lib/src/fixes.dart b/packages/custom_lint_core/lib/src/fixes.dart new file mode 100644 index 00000000..5af24f37 --- /dev/null +++ b/packages/custom_lint_core/lib/src/fixes.dart @@ -0,0 +1,175 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; +import 'package:meta/meta.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:uuid/uuid.dart'; + +import 'change_reporter.dart'; +import 'lint_rule.dart'; +import 'plugin_base.dart'; +import 'resolver.dart'; +import 'runnable.dart'; + +/// Args for [Fix]. +@internal +typedef FixArgs = ({ + ChangeReporter reporter, + AnalysisError analysisError, + List others, +}); + +const _uid = Uuid(); + +/// {@template custom_lint_builder.lint_rule} +/// A base class for defining quick-fixes for a [LintRule] +/// +/// For creating assists inside Dart files, see [DartFix]. +/// Subclassing [Fix] can be helpful if you wish to implement assists for +/// non-Dart files (yaml, json, ...) +/// +/// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/fixes.md +/// {@endtemplate} +@immutable +abstract class Fix extends Runnable { + /// A unique ID for a fix. Must be unique across all fixes of any package. + /// + /// This is used to know which fix triggered a change, for batch support. + late final String id = _uid.v4(); + + /// A list of glob patterns matching the files that [run] cares about. + /// + /// This can include Dart files, Yaml files, ... + List get filesToAnalyze; + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async {} + + @internal + @override + void callRun( + CustomLintResolver resolver, + CustomLintContext context, + FixArgs args, + ) { + run( + resolver, + args.reporter, + context, + args.analysisError, + args.others, + ); + } + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + /// Emits source changes for a given error. + /// + /// Optionally [others] can be specified with a list of similar errors within + /// the same file. + /// This can be used to provide an option for fixing multiple errors at once. + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ); +} + +/// A base class for defining quick-fixes inside Dart files. +/// +/// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/fixes.md#Defining-dart-fix +@immutable +abstract class DartFix extends Fix { + static final _stateKey = Object(); + + @override + List get filesToAnalyze => const ['**.dart']; + + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async { + // Relying on shared state to execute all linters in a single AstVisitor + if (context.sharedState.containsKey(_stateKey)) return; + context.sharedState[_stateKey] = Object(); + + final unit = await resolver.getResolvedUnitResult(); + + context.addPostRunCallback(() { + final linterVisitor = LinterVisitor(context.registry.nodeLintRegistry); + + unit.unit.accept(linterVisitor); + }); + } + + /// Runs this fix in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + /// + /// The parameter [pubspec] can be used to simulate a pubspec file which will + /// be passed to [CustomLintContext.pubspec]. + /// By default, an empty pubspec with the name `test_project` will be used. + @visibleForTesting + Future> testRun( + ResolvedUnitResult result, + AnalysisError analysisError, + List others, { + Pubspec? pubspec, + }) async { + final registry = LintRuleNodeRegistry( + NodeLintRegistry(LintRegistry(), enableTiming: false), + 'unknown', + ); + final postRunCallbacks = []; + final context = CustomLintContext( + registry, + postRunCallbacks.add, + {}, + pubspec, + ); + final resolver = CustomLintResolverImpl( + () => Future.value(result), + lineInfo: result.lineInfo, + path: result.path, + source: result.libraryElement.source, + ); + final reporter = ChangeReporterImpl(result.session, resolver); + + await startUp(resolver, context); + run(resolver, reporter, context, analysisError, others); + runPostRunCallbacks(postRunCallbacks); + + return reporter.complete(); + } + + /// Analyze a Dart file and runs this fix in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + @visibleForTesting + Future> testAnalyzeAndRun( + File file, + AnalysisError analysisError, + List others, + ) async { + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + return testRun(result, analysisError, others); + } +} diff --git a/packages/custom_lint_core/lib/src/lint_codes.dart b/packages/custom_lint_core/lib/src/lint_codes.dart new file mode 100644 index 00000000..c1c60cf5 --- /dev/null +++ b/packages/custom_lint_core/lib/src/lint_codes.dart @@ -0,0 +1,46 @@ +// Forked from package:analyzer/src/dart/error/lint_codes.dart + +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:meta/meta.dart'; + +import '../custom_lint_core.dart'; + +/// A class representing an [ErrorCode] for [LintRule]s. +@immutable +class LintCode extends ErrorCode { + /// A class representing an [ErrorCode] for [LintRule]s. + const LintCode({ + required super.name, + required super.problemMessage, + super.correctionMessage, + String? uniqueName, + this.url, + this.errorSeverity = ErrorSeverity.INFO, + }) : super( + uniqueName: uniqueName ?? name, + ); + + @override + ErrorType get type => ErrorType.LINT; + + @override + final String? url; + + @override + final ErrorSeverity errorSeverity; + + @override + int get hashCode => uniqueName.hashCode; + + @override + bool operator ==(Object other) { + return other is LintCode && uniqueName == other.uniqueName; + } +} diff --git a/packages/custom_lint_core/lib/src/lint_rule.dart b/packages/custom_lint_core/lib/src/lint_rule.dart new file mode 100644 index 00000000..dff02af9 --- /dev/null +++ b/packages/custom_lint_core/lib/src/lint_rule.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart' show AnalysisError; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; +import 'package:meta/meta.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import '../custom_lint_core.dart'; +import 'plugin_base.dart'; +import 'resolver.dart'; + +/// An object for state shared between multiple [LintRule]/[Assist]/[Fix]... +class CustomLintContext { + /// Create a [CustomLintContext]. + @internal + CustomLintContext( + this.registry, + this._addPostRunCallback, + this.sharedState, + Pubspec? pubspec, + ) : pubspec = pubspec ?? Pubspec('test_project'); + + /// An object used to listen to the analysis of a Dart file. + /// + /// Using [registry], we can add listeners to specific [AstNode]s. + /// The listeners will be executed after the `run` function has ended. + final LintRuleNodeRegistry registry; + + /// An object shared with all lint rules/fixes/assits running. + final Map sharedState; + + /// The pubspec of the analyzed project. + /// + /// This can be used to disable a lint rule based on the presence/absence of a dependency. + final Pubspec pubspec; + + final void Function(void Function() cb) _addPostRunCallback; + + /// Registers a function that will be executed after all [LintRule.run] + /// (or [Assist.run]/[Fix.run] if associated to an assist/fix). + void addPostRunCallback(void Function() cb) { + _addPostRunCallback(Zone.current.bindCallback(cb)); + } +} + +/// {@macro custom_lint_builder.lint_rule} +@immutable +abstract class LintRule { + /// {@template custom_lint_builder.lint_rule} + /// A base class for plugins to define emit warnings/errors/infos. + /// + /// For creating assists inside Dart files, see [DartLintRule]. + /// Suclassing [LintRule] can be helpful if you wish to implement assists for + /// non-Dart files (yaml, json, ...) + /// + /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/lints.md + /// {@endtemplate} + const LintRule({required this.code}); + + /// The [LintCode] that this [LintRule] may emit. + /// + /// [LintRule]s should avoid emitting lints that use a code different that [code]. + final LintCode code; + + /// Whether the lint rule is on or off by default in an empty analysis_options.yaml + bool get enabledByDefault => true; + + /// A list of glob patterns matching the files that [run] cares about. + /// + /// This can include Dart files, Yaml files, ... + List get filesToAnalyze; + + /// Checks whether this lint rule is enabled in a configuration file. + /// + /// If a lint is neither enabled nor disabled by a configuration file, + /// [enabledByDefault] will be checked. + bool isEnabled(CustomLintConfigs configs) { + return configs.rules[code.name]?.enabled ?? + configs.enableAllLintRules ?? + enabledByDefault; + } + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async {} + + /// Emits lints for a given file. + /// + /// [run] will only be invoked with files respecting [filesToAnalyze] + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ); + + /// Obtains the list of [Fix] associated with this [LintRule]. + List getFixes() => const []; +} + +/// A base class for emitting warnings/errors/infos inside Dart files. +/// +/// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/lints.md#Defining-dart-lints +@immutable +abstract class DartLintRule extends LintRule { + /// A base class for emitting warnings/errors/infos inside Dart files. + /// + /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/lints.md#Defining-dart-lints + const DartLintRule({required super.code}); + + static final _stateKey = Object(); + + @override + List get filesToAnalyze => const ['**.dart']; + + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ) async { + // Relying on shared state to execute all linters in a single AstVisitor + if (context.sharedState.containsKey(_stateKey)) return; + context.sharedState[_stateKey] = Object(); + + final unit = await resolver.getResolvedUnitResult(); + + context.addPostRunCallback(() { + final linterVisitor = LinterVisitor(context.registry.nodeLintRegistry); + unit.unit.accept(linterVisitor); + }); + } + + /// Runs this assist in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + /// + /// The parameter [pubspec] can be used to simulate a pubspec file which will + /// be passed to [CustomLintContext.pubspec]. + /// By default, an empty pubspec with the name `test_project` will be used. + @visibleForTesting + Future> testRun( + ResolvedUnitResult result, { + Pubspec? pubspec, + }) async { + final registry = LintRuleNodeRegistry( + NodeLintRegistry(LintRegistry(), enableTiming: false), + 'unknown', + ); + final postRunCallbacks = []; + final context = CustomLintContext( + registry, + postRunCallbacks.add, + {}, + pubspec, + ); + final resolver = CustomLintResolverImpl( + () => Future.value(result), + lineInfo: result.lineInfo, + path: result.path, + source: result.libraryElement.source, + ); + + final listener = RecordingErrorListener(); + final reporter = ErrorReporter( + listener, + result.libraryElement.source, + ); + + await startUp(resolver, context); + + run(resolver, reporter, context); + runPostRunCallbacks(postRunCallbacks); + + return listener.errors; + } + + /// Analyze a Dart file and runs this assist in test mode. + /// + /// The result will contain all the changes that would have been applied by [run]. + @visibleForTesting + Future> testAnalyzeAndRun(File file) async { + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + return testRun(result); + } +} diff --git a/packages/custom_lint_core/lib/src/matcher.dart b/packages/custom_lint_core/lib/src/matcher.dart new file mode 100644 index 00000000..b25324a6 --- /dev/null +++ b/packages/custom_lint_core/lib/src/matcher.dart @@ -0,0 +1,269 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:collection/collection.dart'; +import 'package:glob/glob.dart'; +import 'package:matcher/matcher.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +/// Encodes a list of [PrioritizedSourceChange] into a string. +/// +/// This strips the file paths from the json output. +/// +/// {@template encodePrioritizedSourceChanges.args} +/// - [sources] is an optional map of file paths to their content. +/// If specified, the changes will be applied to their corresponding source, +/// and the result will be saved in the diff. +/// Glob syntax is supported in the file paths. +/// - [relativePath] can be specified to change file paths in goldens +/// to be relative to a specific directory. +/// {@endtemplate} +String encodePrioritizedSourceChanges( + Iterable changes, { + JsonEncoder? encoder, + Map? sources, + String? relativePath, +}) { + if (sources != null) { + final buffer = StringBuffer(); + + for (final prioritizedSourceChange in changes) { + buffer.writeln('Message: `${prioritizedSourceChange.change.message}`'); + buffer.writeln('Priority: ${prioritizedSourceChange.priority}'); + if (prioritizedSourceChange.change.id != null) { + buffer.writeln('Id: `${prioritizedSourceChange.change.id}`'); + } + if (prioritizedSourceChange.change.selection case final selection?) { + buffer.writeln( + 'Selection: offset ${selection.offset} ; ' + 'file: `${selection.file}`; ' + 'length: ${prioritizedSourceChange.change.selectionLength}', + ); + } + + final files = prioritizedSourceChange.change.edits + .map((e) => p.normalize(p.relative(e.file, from: relativePath))) + .toSet() + .sortedBy((a) => a); + + for (final file in files) { + final source = sources.entries + .firstWhereOrNull( + (e) => + Glob(e.key).matches(file) || + // workaround to https://github.com/dart-lang/glob/issues/72 + Glob('/${e.key}').matches(file), + ) + ?.value; + if (source == null) { + throw StateError('No source found for file: $file'); + } + + final sourceLineInfo = LineInfo.fromContent(source); + + final output = SourceEdit.applySequence( + source, + prioritizedSourceChange.change.edits + .expand((element) => element.edits), + ); + + final outputLineInfo = LineInfo.fromContent(output); + + // Get the offset of the first changed character between output and source. + var firstDiffOffset = 0; + for (; firstDiffOffset < source.length; firstDiffOffset++) { + if (source[firstDiffOffset] != output[firstDiffOffset]) { + break; + } + } + + // Get the last changed character offset between output and source. + var endSourceOffset = source.length - 1; + var endOutputOffset = output.length - 1; + for (; + endOutputOffset > firstDiffOffset && + endSourceOffset > firstDiffOffset; + endOutputOffset--, endSourceOffset--) { + if (source[endSourceOffset] != output[endOutputOffset]) { + break; + } + } + + final firstChangedLine = + sourceLineInfo.getLocation(firstDiffOffset).lineNumber - 1; + + void writeDiff({ + required String file, + required LineInfo lineInfo, + required int endOffset, + required String token, + required int leadingCount, + required int trailingCount, + }) { + final lastChangedLine = + lineInfo.getLocation(endOffset).lineNumber - 1; + final endLine = + min(lastChangedLine + trailingCount, lineInfo.lineCount - 1); + for (var line = max(0, firstChangedLine - leadingCount); + line <= endLine; + line++) { + final changed = line >= firstChangedLine && line <= lastChangedLine; + if (changed) buffer.write(token); + + final endOfSource = !(line + 1 < lineInfo.lineCount); + + buffer.write( + file.substring( + lineInfo.getOffsetOfLine(line), + endOfSource ? null : lineInfo.getOffsetOfLine(line + 1) - 1, + ), + ); + if (!endOfSource) buffer.writeln(); + } + } + + buffer.writeln('Diff for file `$file:${firstChangedLine + 1}`:'); + buffer.writeln('```'); + writeDiff( + file: source, + lineInfo: sourceLineInfo, + endOffset: endSourceOffset, + leadingCount: 2, + trailingCount: 0, + token: '- ', + ); + + writeDiff( + file: output, + lineInfo: outputLineInfo, + endOffset: endOutputOffset, + leadingCount: 0, + trailingCount: 2, + token: '+ ', + ); + buffer.writeln('```'); + } + + buffer.writeln('---'); + } + + return buffer.toString(); + } + + final json = changes.map((e) => e.toJson()).toList(); + // Remove all "file" references from the json. + for (final change in json) { + final changeMap = change['change']! as Map; + final edits = changeMap['edits']! as List; + for (final edit in edits.cast>()) { + edit.remove('file'); + } + } + + encoder ??= const JsonEncoder.withIndent(' '); + return encoder.convert(json); +} + +/// Expects that a [`List`] matches with a serialized snapshots. +/// +/// This effectively encode the list of changes, remove file paths from the result, +/// and compare this output with the content of a file. +/// +/// {@macro encodePrioritizedSourceChanges.args} +@visibleForTesting +Matcher matcherNormalizedPrioritizedSourceChangeSnapshot( + String filePath, { + JsonEncoder? encoder, + Map? sources, + String? relativePath, +}) { + return _MatcherNormalizedPrioritizedSourceChangeSnapshot( + filePath, + encoder: encoder, + sources: sources, + relativePath: relativePath, + ); +} + +class _MatcherNormalizedPrioritizedSourceChangeSnapshot extends Matcher { + _MatcherNormalizedPrioritizedSourceChangeSnapshot( + this.path, { + this.encoder, + this.sources, + this.relativePath, + }); + + final String path; + final JsonEncoder? encoder; + final Map? sources; + final String? relativePath; + + static final Object _mismatchedValueKey = Object(); + static final Object _expectedKey = Object(); + + @override + bool matches( + covariant Iterable object, + Map matchState, + ) { + final file = p.isRelative(path) + ? File(p.join(Directory.current.path, 'test', path)) + : File(path); + if (!file.existsSync()) { + matchState[_mismatchedValueKey] = 'File not found: $path'; + return false; + } + + final actual = encodePrioritizedSourceChanges( + object, + encoder: encoder, + sources: sources, + relativePath: relativePath, + ); + + final expected = file.readAsStringSync(); + + if (actual != expected) { + matchState[_mismatchedValueKey] = actual; + matchState[_expectedKey] = expected; + return false; + } + + return true; + } + + @override + Description describe(Description description) { + return description.add('to match snapshot at $path'); + } + + @override + Description describeMismatch( + Object? item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + final actualValue = matchState[_mismatchedValueKey] as String?; + if (actualValue != null) { + final expected = matchState[_expectedKey] as String?; + + if (expected != null) { + return mismatchDescription + .add('Expected to match snapshot at $path:\n') + .addDescriptionOf(expected) + .add('\n\nbut was:\n') + .addDescriptionOf(actualValue); + } else { + return mismatchDescription.add(actualValue); + } + } + + return mismatchDescription.add('Unknown mismatch'); + } +} diff --git a/packages/custom_lint_core/lib/src/package_utils.dart b/packages/custom_lint_core/lib/src/package_utils.dart new file mode 100644 index 00000000..d7372de9 --- /dev/null +++ b/packages/custom_lint_core/lib/src/package_utils.dart @@ -0,0 +1,203 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +/// Utilities to help dealing with paths to common package files. +extension PackageIOUtils on Directory { + /// Creates a child [File] from a list of path segments. + File file( + String name, [ + String? name2, + String? name3, + String? name4, + String? name5, + String? name6, + ]) => + File(join(path, name, name2, name3, name4, name5, name6)); + + /// Creates a child [Directory] from a list of path segments. + Directory dir( + String name, [ + String? name2, + String? name3, + String? name4, + String? name5, + String? name6, + ]) => + Directory(join(path, name, name2, name3, name4, name5, name6)); + + /// The `analysis_options.yaml` file. + File get analysisOptions => file('analysis_options.yaml'); + + /// The `pubspec.yaml` file. + File get pubspec => file('pubspec.yaml'); + + /// The `pubspec_overrides.yaml` file. + File get pubspecOverrides => file('pubspec_overrides.yaml'); + + /// The `.dart_tool/package_config.json` file. + File get packageConfig => file('.dart_tool', 'package_config.json'); + + /// The `.dart_tool/pub/workspace_ref.json` file. + File get workspaceRef => file('.dart_tool', 'pub', 'workspace_ref.json'); + + /// Returns a path relative to the given [other]. + String relativeTo(FileSystemEntity other) { + return normalize(relative(path, from: other.path)); + } +} + +/// Try parsing the pubspec of the given directory. +/// +/// If the parsing fails for any reason, returns null. +Pubspec? tryParsePubspecSync(Directory directory) { + try { + return parsePubspecSync(directory); + } catch (_) { + return null; + } +} + +/// Parse the pubspec of the given directory. +/// +/// Throws if the parsing fails, such as if the file is badly formatted or +/// does not exists. +Pubspec parsePubspecSync(Directory directory) { + return Pubspec.parse(directory.pubspec.readAsStringSync()); +} + +/// Try parsing the pubspec of the given directory. +/// +/// If the parsing fails for any reason, returns null. +Future tryParsePubspec(Directory directory) async { + try { + return await parsePubspec(directory); + } catch (_) { + return null; + } +} + +/// Parse the pubspec of the given directory. +/// +/// Throws if the parsing fails, such as if the file is badly formatted or +/// does not exists. +Future parsePubspec(Directory directory) async { + return Pubspec.parse(await directory.pubspec.readAsString()); +} + +/// Try parsing the `pubspec_overrides.yaml` of the given directory. +/// +/// If the parsing fails for any reason, returns null. +Future?> tryParsePubspecOverrides( + Directory directory, +) async { + try { + return await parsePubspecOverrides(directory); + } catch (_) { + return null; + } +} + +/// Parse the `pubspec_overrides.yaml` of the given directory. +/// +/// Throws if the parsing fails, such as if the file is badly formatted or +/// does not exists. +Future> parsePubspecOverrides( + Directory directory, +) async { + final content = await directory.pubspecOverrides.readAsString(); + // Pubspec.parse requires the "name" field to be present, even though + // pubspec_overrides don't have one. So we inject a fake one. + final pubspec = Pubspec.parse(''' +name: tmp +$content +'''); + + return pubspec.dependencyOverrides; +} + +/// Try parsing the package config of the given directory. +/// +/// If the parsing fails for any reason, returns null. +Future tryParsePackageConfig(Directory directory) async { + try { + return await parsePackageConfig(directory); + } catch (_) { + return null; + } +} + +/// Parse the package config of the given directory. +/// +/// Throws if the parsing fails, such as if the file is badly formatted or +/// does not exists. +Future parsePackageConfig(Directory directory) async { + var packageConfigFile = directory.packageConfig; + if (!packageConfigFile.existsSync()) { + final workspaceRefFile = directory.workspaceRef; + final content = workspaceRefFile.readAsStringSync(); + final json = jsonDecode(content) as Map; + final workspaceRoot = json['workspaceRoot'] as String; + final workspacePath = + normalize(join(workspaceRefFile.parent.path, workspaceRoot)); + final workspaceDir = Directory(workspacePath); + packageConfigFile = workspaceDir.packageConfig; + } + + return PackageConfig.parseBytes( + await packageConfigFile.readAsBytes(), + packageConfigFile.uri, + ); +} + +/// Finds the project directory associated with an analysis context root, or null if it is not found +/// +/// This is a folder that contains both a `pubspec.yaml` and a `.dart_tool/package_config.json` file. +/// It is either alongside the analysis_options.yaml file, or in a parent directory. +Directory? tryFindProjectDirectory( + Directory directory, { + Directory? original, +}) { + try { + return findProjectDirectory( + directory, + original: original, + ); + } catch (_) { + return null; + } +} + +/// Finds the project directory associated with an analysis context root +/// +/// This is a folder that contains a `pubspec.yaml` file. +/// It is either alongside the analysis_options.yaml file, or in a parent directory. +Directory findProjectDirectory( + Directory directory, { + Directory? original, +}) { + if (directory.pubspec.existsSync()) { + return directory; + } + + if (directory.parent.uri == directory.uri) { + throw FindProjectError._(original?.path ?? directory.path); + } + + return findProjectDirectory(directory.parent, original: directory); +} + +/// No pubspec.yaml file was found for a plugin. +@internal +class FindProjectError extends FileSystemException { + /// An error that represents the folder [path] where the search for the pubspec started. + FindProjectError._(String path) + : super('Failed to find dart project at $path:\n', path); + + @override + String toString() => message; +} diff --git a/packages/custom_lint_core/lib/src/plugin_base.dart b/packages/custom_lint_core/lib/src/plugin_base.dart new file mode 100644 index 00000000..ac4287cf --- /dev/null +++ b/packages/custom_lint_core/lib/src/plugin_base.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import 'assist.dart'; +import 'configs.dart'; +import 'lint_rule.dart'; + +/// Runs a list of "postRun" callbacks. +/// +/// Errors are caught to ensure all callbacks are executed. +@internal +void runPostRunCallbacks(List postRunCallbacks) { + for (final postCallback in postRunCallbacks) { + try { + postCallback(); + } catch (err, stack) { + Zone.current.handleUncaughtError(err, stack); + // All postCallbacks should execute even if one throw + } + } +} + +/// A base class for custom analyzer plugins +/// +/// If a print is emitted or an exception is uncaught, +abstract class PluginBase { + /// Returns a list of warning/infos/errors for a Dart file. + List getLintRules(CustomLintConfigs configs); + + /// Obtains the list of assists created by this plugin. + List getAssists() => const []; +} diff --git a/packages/custom_lint_core/lib/src/pragmas.dart b/packages/custom_lint_core/lib/src/pragmas.dart new file mode 100644 index 00000000..bf7a8e0d --- /dev/null +++ b/packages/custom_lint_core/lib/src/pragmas.dart @@ -0,0 +1,5 @@ +import 'package:meta/meta.dart'; + +/// Alias for vm:prefer-inline +@internal +const preferInline = pragma('vm:prefer-inline'); diff --git a/packages/custom_lint_core/lib/src/resolver.dart b/packages/custom_lint_core/lib/src/resolver.dart new file mode 100644 index 00000000..dd877b82 --- /dev/null +++ b/packages/custom_lint_core/lib/src/resolver.dart @@ -0,0 +1,56 @@ +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/source/source.dart'; +import 'package:meta/meta.dart'; + +/// A class used to interact with files and possibly emit lints of out it. +/// +/// The file analyzed might not be a Dart file. +abstract class CustomLintResolver { + /// The file path that is being analyzed. + String get path; + + /// The content of the file that is being analyzed. + Source get source; + + /// Line/column/offset metadata about [source]. + LineInfo get lineInfo; + + /// Obtains a decoded representation of a Dart file. + /// + /// It is safe to invoke this method multiple times, as the future is cached. + /// + /// May throw an [InconsistentAnalysisException] + Future getResolvedUnitResult(); +} + +/// The implementation of [CustomLintResolver] +@internal +class CustomLintResolverImpl extends CustomLintResolver { + /// The implementation of [CustomLintResolver] + CustomLintResolverImpl( + this._getResolvedUnitResult, { + required this.lineInfo, + required this.source, + required this.path, + }); + + @override + final LineInfo lineInfo; + + @override + final Source source; + + @override + final String path; + + final Future Function() _getResolvedUnitResult; + + Future? _getResolvedUnitResultFuture; + + @override + Future getResolvedUnitResult() { + return _getResolvedUnitResultFuture ??= _getResolvedUnitResult(); + } +} diff --git a/packages/custom_lint_core/lib/src/runnable.dart b/packages/custom_lint_core/lib/src/runnable.dart new file mode 100644 index 00000000..a37194bd --- /dev/null +++ b/packages/custom_lint_core/lib/src/runnable.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; + +import 'lint_rule.dart'; +import 'resolver.dart'; + +/// A base-class for runnable objects. +abstract class Runnable { + /// Initializes the runnable object. + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ); + + /// Runs the runnable object. + @internal + void callRun( + CustomLintResolver resolver, + CustomLintContext context, + RunArgs args, + ); +} diff --git a/packages/custom_lint_core/lib/src/source_range_extensions.dart b/packages/custom_lint_core/lib/src/source_range_extensions.dart new file mode 100644 index 00000000..0097c192 --- /dev/null +++ b/packages/custom_lint_core/lib/src/source_range_extensions.dart @@ -0,0 +1,18 @@ +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:analyzer/source/source_range.dart'; + +/// Adds [sourceRange] +extension SyntacticEntitySourceRange on SyntacticEntity { + /// A [SourceRange] based on [offset] + [length] + SourceRange get sourceRange => SourceRange(offset, length); +} + +/// Adds [sourceRange] +extension AnalysisErrorSourceRange on AnalysisError { + /// A [SourceRange] based on [offset] + [length] + SourceRange get sourceRange => SourceRange(offset, length); +} diff --git a/packages/custom_lint_core/lib/src/type_checker.dart b/packages/custom_lint_core/lib/src/type_checker.dart new file mode 100644 index 00000000..ffa0725b --- /dev/null +++ b/packages/custom_lint_core/lib/src/type_checker.dart @@ -0,0 +1,492 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Code imported from source_gen + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; + +/// An abstraction around doing static type checking at compile/build time. +abstract class TypeChecker { + const TypeChecker._(); + + /// Creates a new [TypeChecker] that delegates to other [checkers]. + /// + /// This implementation will return `true` for type checks if _any_ of the + /// provided type checkers return true, which is useful for deprecating an + /// API: + /// ```dart + /// const $Foo = const TypeChecker.fromRuntime(Foo); + /// const $Bar = const TypeChecker.fromRuntime(Bar); + /// + /// // Used until $Foo is deleted. + /// const $FooOrBar = const TypeChecker.forAny(const [$Foo, $Bar]); + /// ``` + const factory TypeChecker.any(Iterable checkers) = _AnyChecker; + + /// Creates a new [TypeChecker] that delegates to other [checkers]. + /// + /// This implementation will return `true` if **all** the checkers match. + /// Checkers will be checked in order. + const factory TypeChecker.every(Iterable checkers) = + _EveryChecker; + + /// Create a new [TypeChecker] backed by a static [type]. + const factory TypeChecker.fromStatic(DartType type) = _LibraryTypeChecker; + + /// Checks that the element comes from a specific package. + const factory TypeChecker.fromPackage(String packageName) = _PackageChecker; + + /// Checks that the element has a specific name, and optionally checks that it + /// is defined from a specific package. + /// + /// This is similar to [TypeChecker.fromUrl] but does not rely on exactly where + /// the definition of the element comes from. + /// The downside is that if somehow a package exposes two elements with the + /// same name, there could be a conflict. + const factory TypeChecker.fromName( + String name, { + String? packageName, + }) = _NamedChecker; + + /// Create a new [TypeChecker] backed by a library [url]. + /// + /// Example of referring to a `LinkedHashMap` from `dart:collection`: + /// ```dart + /// const linkedHashMap = const TypeChecker.fromUrl( + /// 'dart:collection#LinkedHashMap', + /// ); + /// ``` + /// + /// **NOTE**: This is considered a more _brittle_ way of determining the type + /// because it relies on knowing the _absolute_ path (i.e. after resolved + /// `export` directives). You should ideally only use `fromUrl` when you know + /// the full path (likely you own/control the package) or it is in a stable + /// package like in the `dart:` SDK. + const factory TypeChecker.fromUrl(dynamic url) = _UriTypeChecker; + + /// Returns the first constant annotating [element] assignable to this type. + /// + /// Otherwise returns `null`. + /// + /// Throws on unresolved annotations unless [throwOnUnresolved] is `false`. + DartObject? firstAnnotationOf( + Element element, { + bool throwOnUnresolved = true, + }) { + if (element.metadata.isEmpty) { + return null; + } + final results = + annotationsOf(element, throwOnUnresolved: throwOnUnresolved); + return results.isEmpty ? null : results.first; + } + + /// Returns if a constant annotating [element] is assignable to this type. + /// + /// Throws on unresolved annotations unless [throwOnUnresolved] is `false`. + bool hasAnnotationOf(Element element, {bool throwOnUnresolved = true}) => + firstAnnotationOf(element, throwOnUnresolved: throwOnUnresolved) != null; + + /// Returns the first constant annotating [element] that is exactly this type. + /// + /// Throws [UnresolvedAnnotationException] on unresolved annotations unless + /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). + DartObject? firstAnnotationOfExact( + Element element, { + bool throwOnUnresolved = true, + }) { + if (element.metadata.isEmpty) { + return null; + } + final results = + annotationsOfExact(element, throwOnUnresolved: throwOnUnresolved); + return results.isEmpty ? null : results.first; + } + + /// Returns if a constant annotating [element] is exactly this type. + /// + /// Throws [UnresolvedAnnotationException] on unresolved annotations unless + /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). + bool hasAnnotationOfExact(Element element, {bool throwOnUnresolved = true}) => + firstAnnotationOfExact(element, throwOnUnresolved: throwOnUnresolved) != + null; + + DartObject? _computeConstantValue( + Element element, + int annotationIndex, { + bool throwOnUnresolved = true, + }) { + final annotation = element.metadata[annotationIndex]; + final result = annotation.computeConstantValue(); + if (result == null && throwOnUnresolved) { + throw UnresolvedAnnotationException._from(element, annotationIndex); + } + return result; + } + + /// Returns annotating constants on [element] assignable to this type. + /// + /// Throws [UnresolvedAnnotationException] on unresolved annotations unless + /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). + Iterable annotationsOf( + Element element, { + bool throwOnUnresolved = true, + }) => + _annotationsWhere( + element, + isAssignableFromType, + throwOnUnresolved: throwOnUnresolved, + ); + + Iterable _annotationsWhere( + Element element, + bool Function(DartType) predicate, { + bool throwOnUnresolved = true, + }) sync* { + for (var i = 0; i < element.metadata.length; i++) { + final value = _computeConstantValue( + element, + i, + throwOnUnresolved: throwOnUnresolved, + ); + if (value?.type != null && predicate(value!.type!)) { + yield value; + } + } + } + + /// Returns annotating constants on [element] of exactly this type. + /// + /// Throws [UnresolvedAnnotationException] on unresolved annotations unless + /// [throwOnUnresolved] is explicitly set to `false` (default is `true`). + Iterable annotationsOfExact( + Element element, { + bool throwOnUnresolved = true, + }) => + _annotationsWhere( + element, + isExactlyType, + throwOnUnresolved: throwOnUnresolved, + ); + + /// Returns `true` if the type of [element] can be assigned to this type. + bool isAssignableFrom(Element element) => + isExactly(element) || + (element is ClassElement && element.allSupertypes.any(isExactlyType)); + + /// Returns `true` if [staticType] can be assigned to this type. + // ignore: avoid_bool_literals_in_conditional_expressions + bool isAssignableFromType(DartType staticType) => staticType.element == null + ? false + : isAssignableFrom(staticType.element!); + + /// Returns `true` if representing the exact same class as [element]. + bool isExactly(Element element); + + /// Returns `true` if representing the exact same type as [staticType]. + bool isExactlyType(DartType staticType) { + final element = staticType.element; + return element != null && isExactly(element); + } + + /// Returns `true` if representing a super class of [element]. + /// + /// This check only takes into account the *extends* hierarchy. If you wish + /// to check mixins and interfaces, use [isAssignableFrom]. + bool isSuperOf(Element element) { + if (element is ClassElement) { + var theSuper = element.supertype; + + do { + if (isExactlyType(theSuper!)) { + return true; + } + + theSuper = theSuper.superclass; + } while (theSuper != null); + } + + return false; + } + + /// Returns `true` if representing a super type of [staticType]. + /// + /// This only takes into account the *extends* hierarchy. If you wish + /// to check mixins and interfaces, use [isAssignableFromType]. + bool isSuperTypeOf(DartType staticType) { + final element = staticType.element; + return element != null && isSuperOf(element); + } +} + +// Checks a static type against another static type; +class _LibraryTypeChecker extends TypeChecker { + const _LibraryTypeChecker(this._type) : super._(); + + final DartType _type; + + @override + bool isExactly(Element element) => + element is ClassElement && element == _type.element; + + @override + String toString() => _urlOfElement(_type.element!); +} + +@immutable +class _PackageChecker extends TypeChecker { + const _PackageChecker(this._packageName) : super._(); + + final String _packageName; + + @override + bool isExactly(Element element) { + final elementLibraryIdentifier = element.library?.identifier; + if (elementLibraryIdentifier == null) return false; + + if (_packageName.startsWith('dart:')) { + return elementLibraryIdentifier == _packageName || + elementLibraryIdentifier.startsWith('$_packageName/'); + } + + return elementLibraryIdentifier.startsWith('package:$_packageName/'); + } + + @override + bool operator ==(Object o) { + return o is _PackageChecker && o._packageName == _packageName; + } + + @override + int get hashCode => Object.hash(runtimeType, _packageName); + + @override + String toString() => _packageName; +} + +@immutable +class _NamedChecker extends TypeChecker { + const _NamedChecker(this._name, {this.packageName}) : super._(); + + final String _name; + final String? packageName; + + @override + bool isExactly(Element element) { + if (element.name != _name) return false; + + // No packageName specified, ignoring it. + if (packageName == null) return true; + + final checker = _PackageChecker(packageName!); + return checker.isExactly(element); + } + + @override + bool operator ==(Object o) { + return o is _NamedChecker && + o._name == _name && + o.packageName == packageName; + } + + @override + int get hashCode => Object.hash(runtimeType, _name, packageName); + + @override + String toString() => '$packageName#$_name'; +} + +// Checks a runtime type against an Uri and Symbol. +@immutable +class _UriTypeChecker extends TypeChecker { + const _UriTypeChecker(dynamic url) + : _url = '$url', + super._(); + + // Precomputed cache of String --> Uri. + static final _cache = Expando(); + + final String _url; + + /// Url as a [Uri] object, lazily constructed. + Uri get uri => _cache[this] ??= _normalizeUrl(Uri.parse(_url)); + + /// Returns whether this type represents the same as [url]. + bool hasSameUrl(dynamic url) => + uri.toString() == + (url is String ? url : _normalizeUrl(url as Uri).toString()); + + @override + bool isExactly(Element element) => hasSameUrl(_urlOfElement(element)); + + @override + bool operator ==(Object o) => o is _UriTypeChecker && o._url == _url; + + @override + int get hashCode => _url.hashCode; + + @override + String toString() => '$uri'; +} + +class _AnyChecker extends TypeChecker { + const _AnyChecker(this._checkers) : super._(); + + final Iterable _checkers; + + @override + bool isExactly(Element element) => _checkers.any((c) => c.isExactly(element)); +} + +class _EveryChecker extends TypeChecker { + const _EveryChecker(this._checkers) : super._(); + + final Iterable _checkers; + + @override + bool isExactly(Element element) { + return _checkers.every((c) => c.isExactly(element)); + } +} + +/// Exception thrown when [TypeChecker] fails to resolve a metadata annotation. +/// +/// Methods such as [TypeChecker.firstAnnotationOf] may throw this exception +/// when one or more annotations are not resolvable. This is usually a sign that +/// something was misspelled, an import is missing, or a dependency was not +/// defined (for build systems such as Bazel). +class UnresolvedAnnotationException implements Exception { + /// Creates an exception from an annotation ([annotationIndex]) that was not + /// resolvable while traversing [Element.metadata] on [annotatedElement]. + factory UnresolvedAnnotationException._from( + Element annotatedElement, + int annotationIndex, + ) { + final sourceSpan = _findSpan(annotatedElement, annotationIndex); + return UnresolvedAnnotationException._(annotatedElement, sourceSpan); + } + + const UnresolvedAnnotationException._( + this.annotatedElement, + this.annotationSource, + ); + + static SourceSpan? _findSpan( + Element annotatedElement, + int annotationIndex, + ) { + final parsedLibrary = annotatedElement.session! + .getParsedLibraryByElement(annotatedElement.library!) + as ParsedLibraryResult; + final declaration = parsedLibrary.getElementDeclaration(annotatedElement); + if (declaration == null) { + return null; + } + final node = declaration.node; + final List metadata; + if (node is AnnotatedNode) { + metadata = node.metadata; + } else if (node is FormalParameter) { + metadata = node.metadata; + } else { + throw StateError( + 'Unhandled Annotated AST node type: ${node.runtimeType}', + ); + } + final annotation = metadata[annotationIndex]; + final start = annotation.offset; + final end = start + annotation.length; + final parsedUnit = declaration.parsedUnit!; + return SourceSpan( + SourceLocation(start, sourceUrl: parsedUnit.uri), + SourceLocation(end, sourceUrl: parsedUnit.uri), + parsedUnit.content.substring(start, end), + ); + } + + /// Element that was annotated with something we could not resolve. + final Element annotatedElement; + + /// Source span of the annotation that was not resolved. + /// + /// May be `null` if the import library was not found. + final SourceSpan? annotationSource; + + @override + String toString() { + final message = 'Could not resolve annotation for `$annotatedElement`.'; + if (annotationSource != null) { + return annotationSource!.message(message); + } + return message; + } +} + +/// Returns a URL representing [element]. +String _urlOfElement(Element element) => element.kind == ElementKind.DYNAMIC + ? 'dart:core#dynamic' + : element.kind == ElementKind.NEVER + ? 'dart:core#Never' + // using librarySource.uri – in case the element is in a part + : _normalizeUrl(element.librarySource!.uri) + .replace(fragment: element.name) + .toString(); + +Uri _normalizeUrl(Uri url) { + switch (url.scheme) { + case 'dart': + return _normalizeDartUrl(url); + case 'package': + return _packageToAssetUrl(url); + case 'file': + return _fileToAssetUrl(url); + default: + return url; + } +} + +/// Make `dart:`-type URLs look like a user-knowable path. +/// +/// Some internal dart: URLs are something like `dart:core/map.dart`. +/// +/// This isn't a user-knowable path, so we strip out extra path segments +/// and only expose `dart:core`. +Uri _normalizeDartUrl(Uri url) => url.pathSegments.isNotEmpty + ? url.replace(pathSegments: url.pathSegments.take(1)) + : url; + +Uri _fileToAssetUrl(Uri url) { + if (!p.isWithin(p.url.current, url.path)) return url; + + return Uri( + scheme: 'asset', + path: p.join('', p.relative(url.path)), + ); +} + +/// Returns a `package:` URL converted to a `asset:` URL. +/// +/// This makes internal comparison logic much easier, but still allows users +/// to define assets in terms of `package:`, which is something that makes more +/// sense to most. +/// +/// For example, this transforms `package:source_gen/source_gen.dart` into: +/// `asset:source_gen/lib/source_gen.dart`. +Uri _packageToAssetUrl(Uri url) => url.scheme == 'package' + ? url.replace( + scheme: 'asset', + pathSegments: [ + url.pathSegments.first, + 'lib', + ...url.pathSegments.skip(1), + ], + ) + : url; diff --git a/packages/custom_lint_core/pubspec.yaml b/packages/custom_lint_core/pubspec.yaml new file mode 100644 index 00000000..fbb872af --- /dev/null +++ b/packages/custom_lint_core/pubspec.yaml @@ -0,0 +1,28 @@ +name: custom_lint_core +version: 0.7.5 +description: A package to help writing custom linters +repository: https://github.com/invertase/dart_custom_lint + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + collection: ^1.16.0 + custom_lint_visitor: ^1.0.0 + glob: ^2.1.2 + matcher: ^0.12.0 + meta: ^1.7.0 + package_config: ^2.1.0 + path: ^1.8.0 + pubspec_parse: ^1.2.2 + source_span: ^1.8.0 + uuid: ^4.5.1 + yaml: ^3.1.1 + +dev_dependencies: + build_runner: ^2.3.3 + lint_visitor_generator: + path: ../lint_visitor_generator + test: ^1.22.2 diff --git a/packages/custom_lint_core/pubspec_overrides.yaml b/packages/custom_lint_core/pubspec_overrides.yaml new file mode 100644 index 00000000..fa38855e --- /dev/null +++ b/packages/custom_lint_core/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: lint_visitor_generator,custom_lint_visitor +dependency_overrides: + custom_lint: + path: ../custom_lint + lint_visitor_generator: + path: ../lint_visitor_generator diff --git a/packages/custom_lint_core/test/assist_test.dart b/packages/custom_lint_core/test/assist_test.dart new file mode 100644 index 00000000..84ea82a0 --- /dev/null +++ b/packages/custom_lint_core/test/assist_test.dart @@ -0,0 +1,233 @@ +import 'dart:io' as io; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_core/src/assist.dart'; +import 'package:custom_lint_core/src/change_reporter.dart'; +import 'package:custom_lint_core/src/lint_rule.dart'; +import 'package:custom_lint_core/src/matcher.dart'; +import 'package:custom_lint_core/src/resolver.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +File writeToTemporaryFile(String content) { + final tempDir = io.Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + + final file = io.File(p.join(tempDir.path, 'file.dart')) + ..createSync(recursive: true) + ..writeAsStringSync(content); + + return file; +} + +void main() { + test('Assist.testRun', () async { + final assist = MyAssist('MyAssist'); + final assist2 = MyAssist('Another'); + + const fileSource = ''' +void main() { + print('Hello world'); +} +'''; + final file = writeToTemporaryFile(fileSource); + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final changes = assist.testRun(result, SourceRange.EMPTY); + final changes2 = assist2.testRun(result, SourceRange.EMPTY); + + expect( + await changes, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + expect( + await changes, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + + expect( + await changes2, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + expect( + await changes2, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + }); + + // Extract the name of the changed file and makes the test failing if the internal structure is not the expected one + String extractFileName(PrioritizedSourceChange change) { + final map = change.toJson(); + + expect(map.containsKey('change'), true); + final changes = map['change']! as Map; + + expect(changes.containsKey('edits'), true); + final edits = changes['edits']! as List; + + expect(edits.length, 1); + final edit = edits[0]! as Map; + + expect(edit.containsKey('file'), true); + final fileName = edit['file'] as String; + + return fileName; + } + + test('CustomAssist.testRun', () async { + final assist1 = MyCustomAssist('CustomAssist', 'custom_1.txt'); + final assist2 = MyCustomAssist('AnotherCustom', 'custom_2.txt'); + + final file = writeToTemporaryFile(''' +void main() { + print('Custom world'); +} +'''); + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final changeList1 = await assist1.testRun(result, SourceRange.EMPTY); + final changeList2 = await assist2.testRun(result, SourceRange.EMPTY); + + final change1 = changeList1[0]; + final change2 = changeList2[0]; + + final file1 = extractFileName(change1); + final file2 = extractFileName(change2); + + expect(file1, 'custom_1.txt'); + expect(file2, 'custom_2.txt'); + }); + + test('Assist.testAnalyzeAndRun', () async { + final assist = MyAssist('MyAssist'); + final assist2 = MyAssist('Another'); + + const fileSource = ''' +void main() { + print('Hello world'); +} +'''; + final file = writeToTemporaryFile(fileSource); + + final changes = assist.testAnalyzeAndRun(file, SourceRange.EMPTY); + final changes2 = assist2.testAnalyzeAndRun(file, SourceRange.EMPTY); + + expect( + await changes, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + expect( + await changes, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + + expect( + await changes2, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + expect( + await changes2, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + }); +} + +class MyAssist extends DartAssist { + MyAssist(this.name); + + final String name; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + context.registry.addMethodInvocation((node) { + final changeBuilder = reporter.createChangeBuilder( + message: name, + priority: 1, + ); + + changeBuilder.addGenericFileEdit((builder) { + builder.addSimpleInsertion(node.offset, 'Hello'); + }); + }); + } +} + +class MyCustomAssist extends DartAssist { + MyCustomAssist(this.name, this.customPath); + + final String name; + final String customPath; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + context.registry.addMethodInvocation((node) { + final changeBuilder = reporter.createChangeBuilder( + message: name, + priority: 1, + ); + + changeBuilder.addGenericFileEdit( + (builder) { + builder.addSimpleInsertion(node.offset, 'Custom 2023'); + }, + customPath: customPath, + ); + }); + } +} diff --git a/packages/custom_lint_core/test/configs_test.dart b/packages/custom_lint_core/test/configs_test.dart new file mode 100644 index 00000000..248be5d2 --- /dev/null +++ b/packages/custom_lint_core/test/configs_test.dart @@ -0,0 +1,451 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +io.Directory createDir() { + final tempDir = io.Directory.systemTemp.createTempSync(); + addTearDown(() async => tempDir.delete(recursive: true)); + return tempDir; +} + +File createAnalysisOptions(String content) { + final dir = createDir(); + final ioFile = io.File(join(dir.path, 'analysis_options.yaml')); + ioFile.writeAsStringSync(content); + return PhysicalResourceProvider.INSTANCE.getFile(ioFile.path); +} + +Future createTempProject({ + required String tempDirPath, + required String projectName, + String? packageConfig, + String? workspaceRef, +}) async { + final projectPath = join(tempDirPath, projectName); + + final dir = io.Directory(projectPath); + await dir.create(); + + final libPath = join(dir.path, 'lib'); + await io.Directory(libPath).create(); + + final analysisOptionsPath = join(libPath, 'analysis_options.yaml'); + await io.File(analysisOptionsPath).writeAsString(''' +custom_lint: + rules: + - from_package + '''); + + final projectDartToolPath = join(projectPath, '.dart_tool'); + await io.Directory(projectDartToolPath).create(recursive: true); + + if (packageConfig != null) { + final String packageConfigPath; + if (workspaceRef != null) { + final workspaceDartToolPath = join(tempDirPath, '.dart_tool'); + await io.Directory(workspaceDartToolPath).create(recursive: true); + packageConfigPath = join(workspaceDartToolPath, 'package_config.json'); + } else { + packageConfigPath = join(projectDartToolPath, 'package_config.json'); + } + await io.File(packageConfigPath).writeAsString(packageConfig); + } + + if (workspaceRef != null) { + final pubPath = join(projectDartToolPath, 'pub'); + await io.Directory(pubPath).create(recursive: true); + final workspaceRefPath = join(pubPath, 'workspace_ref.json'); + await io.File(workspaceRefPath).writeAsString(workspaceRef); + } + + return dir.path; +} + +PackageConfig patchPackageConfig( + PackageConfig currentPackageConfig, + String packageName, + String packagePath, +) { + final patchedPackages = currentPackageConfig.packages.toList() + ..add( + Package( + packageName, + Uri.file('$packagePath/'), + packageUriRoot: Uri.parse('lib/'), + ), + ); + + final patchedPackageConfig = PackageConfig( + patchedPackages, + extraData: currentPackageConfig.extraData, + ); + + return patchedPackageConfig; +} + +void main() async { + late File includeFile; + late CustomLintConfigs includeConfig; + + final packageConfig = await parsePackageConfig(io.Directory.current); + + const testPackageName = 'test_package_with_config'; + setUp(() { + includeFile = createAnalysisOptions( + ''' +custom_lint: + rules: + - a + - b: false + - c: + foo: 42 + - d +''', + ); + + includeConfig = CustomLintConfigs.parse(includeFile, packageConfig); + }); + + test('Empty config', () { + expect(CustomLintConfigs.empty.enableAllLintRules, null); + expect(CustomLintConfigs.empty.rules, isEmpty); + expect(CustomLintConfigs.empty.errors, isEmpty); + }); + + group('parse', () { + test('if file is null, defaults to empty', () { + final configs = CustomLintConfigs.parse(null, packageConfig); + expect(configs, same(CustomLintConfigs.empty)); + }); + + test('if file does not exist, defaults to empty ', () { + final configs = CustomLintConfigs.parse( + PhysicalResourceProvider.INSTANCE.getFile('/this-does-no-exist'), + packageConfig, + ); + expect(configs, same(CustomLintConfigs.empty)); + }); + + test('if custom_lint not present in the option file, clones "include"', () { + final analysisOptions = createAnalysisOptions(''' +include: ${includeFile.path} +linter: + rules: + public_member_api_docs: false +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs, includeConfig); + }); + + test('if custom_lint not present in the option file, clones "include"', () { + final analysisOptions = createAnalysisOptions(''' +include: ${includeFile.path} +linter: + rules: + public_member_api_docs: false +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs, includeConfig); + }); + + test('if custom_lint is present but empty, clones "include"', () { + final analysisOptions = createAnalysisOptions(''' +include: ${includeFile.path} +linter: + rules: + public_member_api_docs: false + +custom_lint: +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs, includeConfig); + }); + + test('has an immutable list of rules', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + rules: + - a +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect( + configs.rules, + {'a': const LintOptions.empty(enabled: true)}, + ); + + expect( + () => configs.rules['b'] = const LintOptions.empty(enabled: true), + throwsUnsupportedError, + ); + }); + + test('has an immutable map of errors', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + a: error +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect( + configs.errors, + {'a': ErrorSeverity.ERROR}, + ); + + expect( + () => configs.errors['a'] = ErrorSeverity.INFO, + throwsUnsupportedError, + ); + }); + + test( + 'if custom_lint is present and defines some properties, merges with "include"', + () { + final analysisOptions = createAnalysisOptions(''' +include: ${includeFile.path} +linter: + rules: + public_member_api_docs: false + +custom_lint: + enable_all_lint_rules: false +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.enableAllLintRules, false); + expect(configs.rules, includeConfig.rules); + }); + + test( + 'if custom_lint.enable_all_lint_rules is not present, uses value from "include"', + () { + final included = createAnalysisOptions(''' +custom_lint: + enable_all_lint_rules: false +'''); + final analysisOptions = createAnalysisOptions(''' +include: ${included.path} +custom_lint: + rules: + - a +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.enableAllLintRules, false); + expect(configs.rules, { + 'a': const LintOptions.empty(enabled: true), + }); + }); + + test('include config using package: uri', () async { + final dir = createDir(); + final file = createAnalysisOptions(''' +include: package:$testPackageName/analysis_options.yaml + '''); + + final tempProjectDir = await createTempProject( + tempDirPath: dir.path, + projectName: testPackageName, + ); + + final patchedPackageConfig = patchPackageConfig( + packageConfig, + testPackageName, + tempProjectDir, + ); + final configs = CustomLintConfigs.parse(file, patchedPackageConfig); + + expect(configs.rules.containsKey('from_package'), true); + }); + + test('if package: uri is not resolved default to empty', () async { + const notExistingFileName = 'this-does-not-exist'; + + final file = createAnalysisOptions(''' +include: package:$testPackageName/$notExistingFileName + '''); + final dir = createDir(); + + final tempProjectDir = await createTempProject( + tempDirPath: dir.path, + projectName: testPackageName, + ); + final patchedPackageConfig = patchPackageConfig( + packageConfig, + testPackageName, + tempProjectDir, + ); + final configs = CustomLintConfigs.parse(file, patchedPackageConfig); + + expect(configs, same(CustomLintConfigs.empty)); + }); + + test('if package: uri is not valid default to empty', () async { + const notExistingPackage = 'this-package-does-not-exists'; + + final file = createAnalysisOptions(''' +include: package:$notExistingPackage/analysis_options.yaml + '''); + final dir = createDir(); + final tempProjectDir = await createTempProject( + tempDirPath: dir.path, + projectName: testPackageName, + ); + + final patchedPackageConfig = patchPackageConfig( + packageConfig, + testPackageName, + tempProjectDir, + ); + final configs = CustomLintConfigs.parse(file, patchedPackageConfig); + + expect(configs, same(CustomLintConfigs.empty)); + }); + + test('if custom_lint.rules is present, merges with "include"', () { + final analysisOptions = createAnalysisOptions(''' +include: ${includeFile.path} +linter: + rules: + public_member_api_docs: false + +custom_lint: + enable_all_lint_rules: true + rules: + - a2 + - b2: false + - c2: + foo: 21 + - d +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.enableAllLintRules, true); + expect(configs.rules, { + 'a': const LintOptions.empty(enabled: true), + 'b': const LintOptions.empty(enabled: false), + 'c': const LintOptions.fromYaml({'foo': 42}, enabled: true), + 'a2': const LintOptions.empty(enabled: true), + 'b2': const LintOptions.empty(enabled: false), + 'c2': const LintOptions.fromYaml({'foo': 21}, enabled: true), + 'd': const LintOptions.empty(enabled: true), + }); + }); + + group('Handles errors', () { + test('Defaults to empty if yaml fails to parse', () { + final configs = CustomLintConfigs.parse( + createAnalysisOptions( + ''' +foo: + bar: + baz: +''', + ), + packageConfig, + ); + expect(configs, CustomLintConfigs.empty); + }); + + test('Parses error severities from configs', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: error + rule_name_2: warning + rule_name_3: info + rule_name_4: none +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.errors, { + 'rule_name_1': ErrorSeverity.ERROR, + 'rule_name_2': ErrorSeverity.WARNING, + 'rule_name_3': ErrorSeverity.INFO, + 'rule_name_4': ErrorSeverity.NONE, + }); + }); + + test('Handles unknown error severity values', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: invalid_severity +'''); + expect( + () => CustomLintConfigs.parse(analysisOptions, packageConfig), + throwsA( + isArgumentError.having( + (e) => e.message, + 'message', + 'Provided error severity: invalid_severity specified for key: rule_name_1 is not valid. ' + 'Valid error severities are: none, info, warning, error', + ), + ), + ); + }); + + test('Merges error severities from included config file', () { + final includedFile = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: error + rule_name_2: warning +'''); + + final analysisOptions = createAnalysisOptions(''' +include: ${includedFile.path} +custom_lint: + errors: + rule_name_2: info + rule_name_3: error +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.errors, { + 'rule_name_1': ErrorSeverity.ERROR, + 'rule_name_2': ErrorSeverity.INFO, + 'rule_name_3': ErrorSeverity.ERROR, + }); + }); + }); + + group('package config', () { + test('single package', () async { + final dir = createDir(); + final projectPath = await createTempProject( + tempDirPath: dir.path, + projectName: testPackageName, + packageConfig: jsonEncode(PackageConfig.toJson(packageConfig)), + ); + final projectDir = io.Directory(projectPath); + final parsed = parsePackageConfig(projectDir); + expect(parsed, isNotNull); + }); + + test('workspace', () async { + final dir = createDir(); + final projectPath = await createTempProject( + tempDirPath: dir.path, + projectName: testPackageName, + packageConfig: jsonEncode(PackageConfig.toJson(packageConfig)), + workspaceRef: jsonEncode({'workspaceRoot': '../../..'}), + ); + final projectDir = io.Directory(projectPath); + final parsed = parsePackageConfig(projectDir); + expect(parsed, isNotNull); + }); + }); + }); +} diff --git a/packages/custom_lint_core/test/fix_test.dart b/packages/custom_lint_core/test/fix_test.dart new file mode 100644 index 00000000..cf344116 --- /dev/null +++ b/packages/custom_lint_core/test/fix_test.dart @@ -0,0 +1,133 @@ +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; +import 'package:custom_lint_core/src/change_reporter.dart'; +import 'package:custom_lint_core/src/fixes.dart'; +import 'package:custom_lint_core/src/lint_rule.dart'; +import 'package:custom_lint_core/src/matcher.dart'; +import 'package:custom_lint_core/src/resolver.dart'; +import 'package:test/test.dart'; + +import 'assist_test.dart'; +import 'lint_rule_test.dart'; + +void main() { + test('Fix.testRun', () async { + final fix = MyFix('MyAssist'); + final fix2 = MyFix('Another'); + + const fileSource = ''' +void main() { + print('Hello world'); +} +'''; + final file = writeToTemporaryFile(fileSource); + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final errors = await const MyLintRule().testRun(result); + + final changes = fix.testRun(result, errors.single, errors); + final changes2 = fix2.testRun(result, errors.single, errors); + + expect( + await changes, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + expect( + await changes, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + + expect( + await changes2, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + expect( + await changes2, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + }); + + test('Fix.testAnalyzeRun', () async { + final fix = MyFix('MyAssist'); + + const fileSource = ''' +void main() { + print('Hello world'); +} +'''; + final file = writeToTemporaryFile(fileSource); + final errors = await const MyLintRule().testAnalyzeAndRun(file); + + final changes = fix.testAnalyzeAndRun(file, errors.single, errors); + + expect( + await changes, + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ); + expect( + await changes, + isNot( + matcherNormalizedPrioritizedSourceChangeSnapshot( + 'snapshot2.diff', + sources: {'**/*': fileSource}, + relativePath: file.parent.path, + ), + ), + ); + }); +} + +class MyFix extends DartFix { + MyFix(this.name); + + final String name; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addMethodInvocation((node) { + final changeBuilder = reporter.createChangeBuilder( + message: name, + priority: 1, + ); + + changeBuilder.addGenericFileEdit((builder) { + builder.addSimpleInsertion(node.offset, 'Hello'); + }); + }); + } +} diff --git a/packages/custom_lint_core/test/lint_rule_test.dart b/packages/custom_lint_core/test/lint_rule_test.dart new file mode 100644 index 00000000..7dfa16e4 --- /dev/null +++ b/packages/custom_lint_core/test/lint_rule_test.dart @@ -0,0 +1,134 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; + +import 'package:test/test.dart'; + +import 'assist_test.dart'; +import 'configs_test.dart'; + +class TestLintRule extends LintRule { + const TestLintRule({required this.enabledByDefault}) + : super( + code: const LintCode( + name: 'test_lint', + problemMessage: 'Test lint', + ), + ); + + @override + List get filesToAnalyze => ['*']; + + @override + final bool enabledByDefault; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) {} +} + +class MyLintRule extends DartLintRule { + const MyLintRule() + : super( + code: const LintCode( + name: 'my_lint_code', + problemMessage: 'message', + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addMethodInvocation((node) { + reporter.atNode(node.methodName, code); + }); + } +} + +void main() async { + const onByDefault = TestLintRule(enabledByDefault: true); + const offByDefault = TestLintRule(enabledByDefault: false); + final packageConfig = await parsePackageConfig(Directory.current); + + test('LintRule.testRun', () async { + const assist = MyLintRule(); + + final file = writeToTemporaryFile(''' +void main() { + print('Hello world'); +} +'''); + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final analysisErrors = await assist.testRun(result); + + expect(analysisErrors, hasLength(1)); + + expect(analysisErrors.first.errorCode.name, 'my_lint_code'); + expect(analysisErrors.first.message, 'message'); + expect(analysisErrors.first.offset, 16); + expect(analysisErrors.first.length, 'print'.length); + }); + + test('LintRule.testAnalyzeAndRun', () async { + const assist = MyLintRule(); + + final file = writeToTemporaryFile(''' +void main() { + print('Hello world'); +} +'''); + + final analysisErrors = await assist.testAnalyzeAndRun(file); + + expect(analysisErrors, hasLength(1)); + + expect(analysisErrors.first.errorCode.name, 'my_lint_code'); + expect(analysisErrors.first.message, 'message'); + expect(analysisErrors.first.offset, 16); + expect(analysisErrors.first.length, 'print'.length); + }); + + group('LintRule.isEnabled', () { + test('defaults to checking "enabedByDefault"', () { + expect(onByDefault.isEnabled(CustomLintConfigs.empty), true); + expect(offByDefault.isEnabled(CustomLintConfigs.empty), false); + }); + + test('always enabled if on in the config files', () { + final analysisOptionFile = createAnalysisOptions(''' +custom_lint: + rules: + - test_lint +'''); + final configs = + CustomLintConfigs.parse(analysisOptionFile, packageConfig); + + expect(onByDefault.isEnabled(configs), true); + expect(offByDefault.isEnabled(configs), true); + }); + + test('always disabled if off in the config files', () { + final analysisOptionFile = createAnalysisOptions(''' +custom_lint: + rules: + - test_lint: false +'''); + final configs = + CustomLintConfigs.parse(analysisOptionFile, packageConfig); + + expect(onByDefault.isEnabled(configs), false); + expect(offByDefault.isEnabled(configs), false); + }); + }); +} diff --git a/packages/custom_lint_core/test/snapshot.diff b/packages/custom_lint_core/test/snapshot.diff new file mode 100644 index 00000000..744a9680 --- /dev/null +++ b/packages/custom_lint_core/test/snapshot.diff @@ -0,0 +1,10 @@ +Message: `MyAssist` +Priority: 1 +Diff for file `file.dart:2`: +``` +void main() { +- print('Hello world'); ++ Helloprint('Hello world'); +} +``` +--- diff --git a/packages/custom_lint_core/test/snapshot2.diff b/packages/custom_lint_core/test/snapshot2.diff new file mode 100644 index 00000000..21299764 --- /dev/null +++ b/packages/custom_lint_core/test/snapshot2.diff @@ -0,0 +1,10 @@ +Message: `Another` +Priority: 1 +Diff for file `file.dart:2`: +``` +void main() { +- print('Hello world'); ++ Helloprint('Hello world'); +} +``` +--- diff --git a/packages/custom_lint_core/test/type_checker_test.dart b/packages/custom_lint_core/test/type_checker_test.dart new file mode 100644 index 00000000..86ee699a --- /dev/null +++ b/packages/custom_lint_core/test/type_checker_test.dart @@ -0,0 +1,205 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +import 'assist_test.dart'; + +void main() { + test('Can call isExactlyType in DartTypes with no element', () async { + final file = writeToTemporaryFile(''' +void main() { + void Function()? fn; + fn?.call(); +} +'''); + + final unit = await resolveFile2(path: file.path); + unit as ResolvedUnitResult; + + const checker = TypeChecker.fromName('foo'); + + unit.unit.accept( + _MethodInvocationVisitor((node) { + expect( + checker.isExactlyType(node.target!.staticType!), + isFalse, + ); + }), + ); + }); + + test('Can call isSuperTypeOf in DartTypes with no element', () async { + final file = writeToTemporaryFile(r''' +void fn((int, String) record) { + final first = record.$1; +} +'''); + + final unit = await resolveFile2(path: file.path); + unit as ResolvedUnitResult; + + const checker = TypeChecker.fromName('record'); + + late final PropertyAccess propertyAccessNode; + unit.unit.accept( + _PropertyAccessVisitor((node) { + propertyAccessNode = node; + }), + ); + + expect(propertyAccessNode.realTarget.staticType!.element, isNull); + expect( + checker.isExactlyType(propertyAccessNode.realTarget.staticType!), + isFalse, + ); + }); + + group('TypeChecker.fromPackage', () { + test('matches a type from a package', () async { + final tempDir = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + + final file = File(join(tempDir.path, 'lib', 'main.dart')) + ..createSync(recursive: true) + ..writeAsStringSync(''' +class Foo {} +int a; +'''); + + final pubspec = File(join(tempDir.path, 'pubspec.yaml')); + pubspec.writeAsStringSync(''' +name: some_package +version: 0.2.8 +description: A package to help writing custom linters +repository: https://github.com/invertase/dart_custom_lint +environment: + sdk: ">=3.0.0 <4.0.0" +'''); + + await Process.run( + 'dart', + ['pub', 'get', '--offline'], + workingDirectory: tempDir.path, + ); + + final unit = await resolveFile2(path: file.path); + unit as ResolvedUnitResult; + + const checker = TypeChecker.fromPackage('some_package'); + const checker2 = TypeChecker.fromPackage('some_package2'); + + expect( + checker.isExactlyType( + (unit.unit.declarations.first as ClassDeclaration) + .declaredElement! + .thisType, + ), + true, + ); + expect( + checker2.isExactlyType( + (unit.unit.declarations.first as ClassDeclaration) + .declaredElement! + .thisType, + ), + false, + ); + + expect( + checker.isExactlyType( + (unit.unit.declarations[1] as TopLevelVariableDeclaration) + .variables + .type! + .type!, + ), + false, + ); + }); + + test('matches a type from a built-in dart: package', () async { + final file = writeToTemporaryFile(''' +import 'dart:io'; + +int a; +File? x; +'''); + + final unit = await resolveFile2(path: file.path); + unit as ResolvedUnitResult; + + const checker = TypeChecker.fromPackage('dart:core'); + const checker2 = TypeChecker.fromPackage('dart:io'); + const checker3 = TypeChecker.fromPackage('some_package'); + + expect( + checker.isExactlyType( + (unit.unit.declarations.first as TopLevelVariableDeclaration) + .variables + .type! + .type!, + ), + true, + ); + + expect( + checker.isExactlyType( + (unit.unit.declarations[1] as TopLevelVariableDeclaration) + .variables + .type! + .type!, + ), + false, + ); + + expect( + checker2.isExactlyType( + (unit.unit.declarations[1] as TopLevelVariableDeclaration) + .variables + .type! + .type!, + ), + true, + ); + + expect( + checker3.isExactlyType( + (unit.unit.declarations.first as TopLevelVariableDeclaration) + .variables + .type! + .type!, + ), + false, + ); + }); + }); +} + +class _MethodInvocationVisitor extends RecursiveAstVisitor { + _MethodInvocationVisitor(this.onMethodInvocation); + + final void Function(MethodInvocation node) onMethodInvocation; + + @override + void visitMethodInvocation(MethodInvocation node) { + onMethodInvocation(node); + super.visitMethodInvocation(node); + } +} + +class _PropertyAccessVisitor extends RecursiveAstVisitor { + _PropertyAccessVisitor(this.onPropertyAccess); + + final void Function(PropertyAccess node) onPropertyAccess; + + @override + void visitPropertyAccess(PropertyAccess node) { + onPropertyAccess(node); + super.visitPropertyAccess(node); + } +} diff --git a/packages/custom_lint_visitor/CHANGELOG.md b/packages/custom_lint_visitor/CHANGELOG.md new file mode 100644 index 00000000..9125f221 --- /dev/null +++ b/packages/custom_lint_visitor/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0+ (6.7.0/6.11.0/7.3.0) + +Initial release diff --git a/packages/custom_lint_visitor/LICENSE b/packages/custom_lint_visitor/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/packages/custom_lint_visitor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/custom_lint_visitor/README.md b/packages/custom_lint_visitor/README.md new file mode 100644 index 00000000..2e02d64f --- /dev/null +++ b/packages/custom_lint_visitor/README.md @@ -0,0 +1,27 @@ +

+

custom_lint_core

+ An package exposing base classes for defining lint rules/fixes/assists. +

+ +

+ License +

+ +## About + +`custom_lint_visitor` is a dependency of `custom_lint`, for the sake of supporting +multiple Analyzer versions without causing too many breaking changes. + +It exposes various ways to traverse the tree of `AstNode`s using callbacks. + +## Versioning + +One version of `custom_lint_visitor` is released for every `analyzer` version. + +The version `1.0.0+6.7.0` means "Version 1.0.0 of custom_lint_visitor, for analyzer's 6.7.0 version". + +Whenever `custom_lint_visitor` is updated, a new version may be published for the same `analyzer` version. Such as `1.0.1+6.7.0` + +Depending on `custom_lint_visitor: ^1.0.0` will therefore support +any compatible Analyzer version. +To require a specific analyzer version, specify `analyzer: ` explicitly. \ No newline at end of file diff --git a/packages/custom_lint_visitor/build.yaml b/packages/custom_lint_visitor/build.yaml new file mode 100644 index 00000000..6cb1b3de --- /dev/null +++ b/packages/custom_lint_visitor/build.yaml @@ -0,0 +1,12 @@ +targets: + $default: + builders: + lint_visitor_generator: + enabled: true + generate_for: + include: + - "**/node_lint_visitor.dart" + source_gen|combining_builder: + options: + ignore_for_file: + - "type=lint" diff --git a/packages/custom_lint_visitor/lib/custom_lint_visitor.dart b/packages/custom_lint_visitor/lib/custom_lint_visitor.dart new file mode 100644 index 00000000..daf78aad --- /dev/null +++ b/packages/custom_lint_visitor/lib/custom_lint_visitor.dart @@ -0,0 +1 @@ +export 'src/node_lint_visitor.dart'; diff --git a/packages/custom_lint_visitor/lib/src/node_lint_visitor.dart b/packages/custom_lint_visitor/lib/src/node_lint_visitor.dart new file mode 100644 index 00000000..2c58302c --- /dev/null +++ b/packages/custom_lint_visitor/lib/src/node_lint_visitor.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'pragmas.dart'; + +part 'node_lint_visitor.g.dart'; + +/// Manages lint timing. +class LintRegistry { + /// Dictionary mapping lints (by name) to timers. + final Map timers = HashMap(); + + /// Get a timer associated with the given lint rule (or create one if none + /// exists). + Stopwatch getTimer(String name) { + return timers.putIfAbsent(name, Stopwatch.new); + } +} diff --git a/packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart b/packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart new file mode 100644 index 00000000..76036518 --- /dev/null +++ b/packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart @@ -0,0 +1,3745 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: type=lint + +part of 'node_lint_visitor.dart'; + +// ************************************************************************** +// _LintVisitorGenerator +// ************************************************************************** + +/// The AST visitor that runs handlers for nodes from the [_registry]. +class LinterVisitor extends GeneralizingAstVisitor { + /// The AST visitor that runs handlers for nodes from the [_registry]. + LinterVisitor(this._registry); + + final NodeLintRegistry _registry; + + void _runSubscriptions( + T node, + List<_Subscription> subscriptions, + ) { + for (var i = 0; i < subscriptions.length; i++) { + final subscription = subscriptions[i]; + final timer = subscription.timer; + timer?.start(); + try { + subscription.zone.runUnary(subscription.listener, node); + } catch (exception, stackTrace) { + subscription.zone.handleUncaughtError(exception, stackTrace); + } + timer?.stop(); + } + } + + @override + void visitAdjacentStrings(AdjacentStrings node) { + _runSubscriptions(node, _registry._forAdjacentStrings); + super.visitAdjacentStrings(node); + } + + @override + void visitAnnotatedNode(AnnotatedNode node) { + _runSubscriptions(node, _registry._forAnnotatedNode); + super.visitAnnotatedNode(node); + } + + @override + void visitAnnotation(Annotation node) { + _runSubscriptions(node, _registry._forAnnotation); + super.visitAnnotation(node); + } + + @override + void visitArgumentList(ArgumentList node) { + _runSubscriptions(node, _registry._forArgumentList); + super.visitArgumentList(node); + } + + @override + void visitAsExpression(AsExpression node) { + _runSubscriptions(node, _registry._forAsExpression); + super.visitAsExpression(node); + } + + @override + void visitAssertInitializer(AssertInitializer node) { + _runSubscriptions(node, _registry._forAssertInitializer); + super.visitAssertInitializer(node); + } + + @override + void visitAssertStatement(AssertStatement node) { + _runSubscriptions(node, _registry._forAssertStatement); + super.visitAssertStatement(node); + } + + @override + void visitAssignedVariablePattern(AssignedVariablePattern node) { + _runSubscriptions(node, _registry._forAssignedVariablePattern); + super.visitAssignedVariablePattern(node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _runSubscriptions(node, _registry._forAssignmentExpression); + super.visitAssignmentExpression(node); + } + + @override + void visitAugmentedExpression(AugmentedExpression node) { + _runSubscriptions(node, _registry._forAugmentedExpression); + super.visitAugmentedExpression(node); + } + + @override + void visitAugmentedInvocation(AugmentedInvocation node) { + _runSubscriptions(node, _registry._forAugmentedInvocation); + super.visitAugmentedInvocation(node); + } + + @override + void visitAwaitExpression(AwaitExpression node) { + _runSubscriptions(node, _registry._forAwaitExpression); + super.visitAwaitExpression(node); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + _runSubscriptions(node, _registry._forBinaryExpression); + super.visitBinaryExpression(node); + } + + @override + void visitBlock(Block node) { + _runSubscriptions(node, _registry._forBlock); + super.visitBlock(node); + } + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + _runSubscriptions(node, _registry._forBlockFunctionBody); + super.visitBlockFunctionBody(node); + } + + @override + void visitBooleanLiteral(BooleanLiteral node) { + _runSubscriptions(node, _registry._forBooleanLiteral); + super.visitBooleanLiteral(node); + } + + @override + void visitBreakStatement(BreakStatement node) { + _runSubscriptions(node, _registry._forBreakStatement); + super.visitBreakStatement(node); + } + + @override + void visitCascadeExpression(CascadeExpression node) { + _runSubscriptions(node, _registry._forCascadeExpression); + super.visitCascadeExpression(node); + } + + @override + void visitCaseClause(CaseClause node) { + _runSubscriptions(node, _registry._forCaseClause); + super.visitCaseClause(node); + } + + @override + void visitCastPattern(CastPattern node) { + _runSubscriptions(node, _registry._forCastPattern); + super.visitCastPattern(node); + } + + @override + void visitCatchClause(CatchClause node) { + _runSubscriptions(node, _registry._forCatchClause); + super.visitCatchClause(node); + } + + @override + void visitCatchClauseParameter(CatchClauseParameter node) { + _runSubscriptions(node, _registry._forCatchClauseParameter); + super.visitCatchClauseParameter(node); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + _runSubscriptions(node, _registry._forClassDeclaration); + super.visitClassDeclaration(node); + } + + @override + void visitClassMember(ClassMember node) { + _runSubscriptions(node, _registry._forClassMember); + super.visitClassMember(node); + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + _runSubscriptions(node, _registry._forClassTypeAlias); + super.visitClassTypeAlias(node); + } + + @override + void visitCollectionElement(CollectionElement node) { + _runSubscriptions(node, _registry._forCollectionElement); + super.visitCollectionElement(node); + } + + @override + void visitCombinator(Combinator node) { + _runSubscriptions(node, _registry._forCombinator); + super.visitCombinator(node); + } + + @override + void visitComment(Comment node) { + _runSubscriptions(node, _registry._forComment); + super.visitComment(node); + } + + @override + void visitCommentReference(CommentReference node) { + _runSubscriptions(node, _registry._forCommentReference); + super.visitCommentReference(node); + } + + @override + void visitCompilationUnit(CompilationUnit node) { + _runSubscriptions(node, _registry._forCompilationUnit); + super.visitCompilationUnit(node); + } + + @override + void visitCompilationUnitMember(CompilationUnitMember node) { + _runSubscriptions(node, _registry._forCompilationUnitMember); + super.visitCompilationUnitMember(node); + } + + @override + void visitConditionalExpression(ConditionalExpression node) { + _runSubscriptions(node, _registry._forConditionalExpression); + super.visitConditionalExpression(node); + } + + @override + void visitConfiguration(Configuration node) { + _runSubscriptions(node, _registry._forConfiguration); + super.visitConfiguration(node); + } + + @override + void visitConstantPattern(ConstantPattern node) { + _runSubscriptions(node, _registry._forConstantPattern); + super.visitConstantPattern(node); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + _runSubscriptions(node, _registry._forConstructorDeclaration); + super.visitConstructorDeclaration(node); + } + + @override + void visitConstructorFieldInitializer(ConstructorFieldInitializer node) { + _runSubscriptions(node, _registry._forConstructorFieldInitializer); + super.visitConstructorFieldInitializer(node); + } + + @override + void visitConstructorInitializer(ConstructorInitializer node) { + _runSubscriptions(node, _registry._forConstructorInitializer); + super.visitConstructorInitializer(node); + } + + @override + void visitConstructorName(ConstructorName node) { + _runSubscriptions(node, _registry._forConstructorName); + super.visitConstructorName(node); + } + + @override + void visitConstructorReference(ConstructorReference node) { + _runSubscriptions(node, _registry._forConstructorReference); + super.visitConstructorReference(node); + } + + @override + void visitConstructorSelector(ConstructorSelector node) { + _runSubscriptions(node, _registry._forConstructorSelector); + super.visitConstructorSelector(node); + } + + @override + void visitContinueStatement(ContinueStatement node) { + _runSubscriptions(node, _registry._forContinueStatement); + super.visitContinueStatement(node); + } + + @override + void visitDartPattern(DartPattern node) { + _runSubscriptions(node, _registry._forDartPattern); + super.visitDartPattern(node); + } + + @override + void visitDeclaration(Declaration node) { + _runSubscriptions(node, _registry._forDeclaration); + super.visitDeclaration(node); + } + + @override + void visitDeclaredIdentifier(DeclaredIdentifier node) { + _runSubscriptions(node, _registry._forDeclaredIdentifier); + super.visitDeclaredIdentifier(node); + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + _runSubscriptions(node, _registry._forDeclaredVariablePattern); + super.visitDeclaredVariablePattern(node); + } + + @override + void visitDefaultFormalParameter(DefaultFormalParameter node) { + _runSubscriptions(node, _registry._forDefaultFormalParameter); + super.visitDefaultFormalParameter(node); + } + + @override + void visitDirective(Directive node) { + _runSubscriptions(node, _registry._forDirective); + super.visitDirective(node); + } + + @override + void visitDoStatement(DoStatement node) { + _runSubscriptions(node, _registry._forDoStatement); + super.visitDoStatement(node); + } + + @override + void visitDottedName(DottedName node) { + _runSubscriptions(node, _registry._forDottedName); + super.visitDottedName(node); + } + + @override + void visitDoubleLiteral(DoubleLiteral node) { + _runSubscriptions(node, _registry._forDoubleLiteral); + super.visitDoubleLiteral(node); + } + + @override + void visitEmptyFunctionBody(EmptyFunctionBody node) { + _runSubscriptions(node, _registry._forEmptyFunctionBody); + super.visitEmptyFunctionBody(node); + } + + @override + void visitEmptyStatement(EmptyStatement node) { + _runSubscriptions(node, _registry._forEmptyStatement); + super.visitEmptyStatement(node); + } + + @override + void visitEnumConstantArguments(EnumConstantArguments node) { + _runSubscriptions(node, _registry._forEnumConstantArguments); + super.visitEnumConstantArguments(node); + } + + @override + void visitEnumConstantDeclaration(EnumConstantDeclaration node) { + _runSubscriptions(node, _registry._forEnumConstantDeclaration); + super.visitEnumConstantDeclaration(node); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + _runSubscriptions(node, _registry._forEnumDeclaration); + super.visitEnumDeclaration(node); + } + + @override + void visitExportDirective(ExportDirective node) { + _runSubscriptions(node, _registry._forExportDirective); + super.visitExportDirective(node); + } + + @override + void visitExpression(Expression node) { + _runSubscriptions(node, _registry._forExpression); + super.visitExpression(node); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _runSubscriptions(node, _registry._forExpressionFunctionBody); + super.visitExpressionFunctionBody(node); + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + _runSubscriptions(node, _registry._forExpressionStatement); + super.visitExpressionStatement(node); + } + + @override + void visitExtendsClause(ExtendsClause node) { + _runSubscriptions(node, _registry._forExtendsClause); + super.visitExtendsClause(node); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + _runSubscriptions(node, _registry._forExtensionDeclaration); + super.visitExtensionDeclaration(node); + } + + @override + void visitExtensionOnClause(ExtensionOnClause node) { + _runSubscriptions(node, _registry._forExtensionOnClause); + super.visitExtensionOnClause(node); + } + + @override + void visitExtensionOverride(ExtensionOverride node) { + _runSubscriptions(node, _registry._forExtensionOverride); + super.visitExtensionOverride(node); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + _runSubscriptions(node, _registry._forExtensionTypeDeclaration); + super.visitExtensionTypeDeclaration(node); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + _runSubscriptions(node, _registry._forFieldDeclaration); + super.visitFieldDeclaration(node); + } + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + _runSubscriptions(node, _registry._forFieldFormalParameter); + super.visitFieldFormalParameter(node); + } + + @override + void visitForEachParts(ForEachParts node) { + _runSubscriptions(node, _registry._forForEachParts); + super.visitForEachParts(node); + } + + @override + void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { + _runSubscriptions(node, _registry._forForEachPartsWithDeclaration); + super.visitForEachPartsWithDeclaration(node); + } + + @override + void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { + _runSubscriptions(node, _registry._forForEachPartsWithIdentifier); + super.visitForEachPartsWithIdentifier(node); + } + + @override + void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { + _runSubscriptions(node, _registry._forForEachPartsWithPattern); + super.visitForEachPartsWithPattern(node); + } + + @override + void visitForElement(ForElement node) { + _runSubscriptions(node, _registry._forForElement); + super.visitForElement(node); + } + + @override + void visitFormalParameter(FormalParameter node) { + _runSubscriptions(node, _registry._forFormalParameter); + super.visitFormalParameter(node); + } + + @override + void visitFormalParameterList(FormalParameterList node) { + _runSubscriptions(node, _registry._forFormalParameterList); + super.visitFormalParameterList(node); + } + + @override + void visitForParts(ForParts node) { + _runSubscriptions(node, _registry._forForParts); + super.visitForParts(node); + } + + @override + void visitForPartsWithDeclarations(ForPartsWithDeclarations node) { + _runSubscriptions(node, _registry._forForPartsWithDeclarations); + super.visitForPartsWithDeclarations(node); + } + + @override + void visitForPartsWithExpression(ForPartsWithExpression node) { + _runSubscriptions(node, _registry._forForPartsWithExpression); + super.visitForPartsWithExpression(node); + } + + @override + void visitForPartsWithPattern(ForPartsWithPattern node) { + _runSubscriptions(node, _registry._forForPartsWithPattern); + super.visitForPartsWithPattern(node); + } + + @override + void visitForStatement(ForStatement node) { + _runSubscriptions(node, _registry._forForStatement); + super.visitForStatement(node); + } + + @override + void visitFunctionBody(FunctionBody node) { + _runSubscriptions(node, _registry._forFunctionBody); + super.visitFunctionBody(node); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _runSubscriptions(node, _registry._forFunctionDeclaration); + super.visitFunctionDeclaration(node); + } + + @override + void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { + _runSubscriptions(node, _registry._forFunctionDeclarationStatement); + super.visitFunctionDeclarationStatement(node); + } + + @override + void visitFunctionExpression(FunctionExpression node) { + _runSubscriptions(node, _registry._forFunctionExpression); + super.visitFunctionExpression(node); + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + _runSubscriptions(node, _registry._forFunctionExpressionInvocation); + super.visitFunctionExpressionInvocation(node); + } + + @override + void visitFunctionReference(FunctionReference node) { + _runSubscriptions(node, _registry._forFunctionReference); + super.visitFunctionReference(node); + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + _runSubscriptions(node, _registry._forFunctionTypeAlias); + super.visitFunctionTypeAlias(node); + } + + @override + void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { + _runSubscriptions(node, _registry._forFunctionTypedFormalParameter); + super.visitFunctionTypedFormalParameter(node); + } + + @override + void visitGenericFunctionType(GenericFunctionType node) { + _runSubscriptions(node, _registry._forGenericFunctionType); + super.visitGenericFunctionType(node); + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + _runSubscriptions(node, _registry._forGenericTypeAlias); + super.visitGenericTypeAlias(node); + } + + @override + void visitGuardedPattern(GuardedPattern node) { + _runSubscriptions(node, _registry._forGuardedPattern); + super.visitGuardedPattern(node); + } + + @override + void visitHideCombinator(HideCombinator node) { + _runSubscriptions(node, _registry._forHideCombinator); + super.visitHideCombinator(node); + } + + @override + void visitIdentifier(Identifier node) { + _runSubscriptions(node, _registry._forIdentifier); + super.visitIdentifier(node); + } + + @override + void visitIfElement(IfElement node) { + _runSubscriptions(node, _registry._forIfElement); + super.visitIfElement(node); + } + + @override + void visitIfStatement(IfStatement node) { + _runSubscriptions(node, _registry._forIfStatement); + super.visitIfStatement(node); + } + + @override + void visitImplementsClause(ImplementsClause node) { + _runSubscriptions(node, _registry._forImplementsClause); + super.visitImplementsClause(node); + } + + @override + void visitImplicitCallReference(ImplicitCallReference node) { + _runSubscriptions(node, _registry._forImplicitCallReference); + super.visitImplicitCallReference(node); + } + + @override + void visitImportDirective(ImportDirective node) { + _runSubscriptions(node, _registry._forImportDirective); + super.visitImportDirective(node); + } + + @override + void visitImportPrefixReference(ImportPrefixReference node) { + _runSubscriptions(node, _registry._forImportPrefixReference); + super.visitImportPrefixReference(node); + } + + @override + void visitIndexExpression(IndexExpression node) { + _runSubscriptions(node, _registry._forIndexExpression); + super.visitIndexExpression(node); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + _runSubscriptions(node, _registry._forInstanceCreationExpression); + super.visitInstanceCreationExpression(node); + } + + @override + void visitIntegerLiteral(IntegerLiteral node) { + _runSubscriptions(node, _registry._forIntegerLiteral); + super.visitIntegerLiteral(node); + } + + @override + void visitInterpolationElement(InterpolationElement node) { + _runSubscriptions(node, _registry._forInterpolationElement); + super.visitInterpolationElement(node); + } + + @override + void visitInterpolationExpression(InterpolationExpression node) { + _runSubscriptions(node, _registry._forInterpolationExpression); + super.visitInterpolationExpression(node); + } + + @override + void visitInterpolationString(InterpolationString node) { + _runSubscriptions(node, _registry._forInterpolationString); + super.visitInterpolationString(node); + } + + @override + void visitInvocationExpression(InvocationExpression node) { + _runSubscriptions(node, _registry._forInvocationExpression); + super.visitInvocationExpression(node); + } + + @override + void visitIsExpression(IsExpression node) { + _runSubscriptions(node, _registry._forIsExpression); + super.visitIsExpression(node); + } + + @override + void visitLabel(Label node) { + _runSubscriptions(node, _registry._forLabel); + super.visitLabel(node); + } + + @override + void visitLabeledStatement(LabeledStatement node) { + _runSubscriptions(node, _registry._forLabeledStatement); + super.visitLabeledStatement(node); + } + + @override + void visitLibraryDirective(LibraryDirective node) { + _runSubscriptions(node, _registry._forLibraryDirective); + super.visitLibraryDirective(node); + } + + @override + void visitLibraryIdentifier(LibraryIdentifier node) { + _runSubscriptions(node, _registry._forLibraryIdentifier); + super.visitLibraryIdentifier(node); + } + + @override + void visitListLiteral(ListLiteral node) { + _runSubscriptions(node, _registry._forListLiteral); + super.visitListLiteral(node); + } + + @override + void visitListPattern(ListPattern node) { + _runSubscriptions(node, _registry._forListPattern); + super.visitListPattern(node); + } + + @override + void visitLiteral(Literal node) { + _runSubscriptions(node, _registry._forLiteral); + super.visitLiteral(node); + } + + @override + void visitLogicalAndPattern(LogicalAndPattern node) { + _runSubscriptions(node, _registry._forLogicalAndPattern); + super.visitLogicalAndPattern(node); + } + + @override + void visitLogicalOrPattern(LogicalOrPattern node) { + _runSubscriptions(node, _registry._forLogicalOrPattern); + super.visitLogicalOrPattern(node); + } + + @override + void visitMapLiteralEntry(MapLiteralEntry node) { + _runSubscriptions(node, _registry._forMapLiteralEntry); + super.visitMapLiteralEntry(node); + } + + @override + void visitMapPattern(MapPattern node) { + _runSubscriptions(node, _registry._forMapPattern); + super.visitMapPattern(node); + } + + @override + void visitMapPatternEntry(MapPatternEntry node) { + _runSubscriptions(node, _registry._forMapPatternEntry); + super.visitMapPatternEntry(node); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + _runSubscriptions(node, _registry._forMethodDeclaration); + super.visitMethodDeclaration(node); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + _runSubscriptions(node, _registry._forMethodInvocation); + super.visitMethodInvocation(node); + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + _runSubscriptions(node, _registry._forMixinDeclaration); + super.visitMixinDeclaration(node); + } + + @override + void visitMixinOnClause(MixinOnClause node) { + _runSubscriptions(node, _registry._forMixinOnClause); + super.visitMixinOnClause(node); + } + + @override + void visitNamedCompilationUnitMember(NamedCompilationUnitMember node) { + _runSubscriptions(node, _registry._forNamedCompilationUnitMember); + super.visitNamedCompilationUnitMember(node); + } + + @override + void visitNamedExpression(NamedExpression node) { + _runSubscriptions(node, _registry._forNamedExpression); + super.visitNamedExpression(node); + } + + @override + void visitNamedType(NamedType node) { + _runSubscriptions(node, _registry._forNamedType); + super.visitNamedType(node); + } + + @override + void visitNamespaceDirective(NamespaceDirective node) { + _runSubscriptions(node, _registry._forNamespaceDirective); + super.visitNamespaceDirective(node); + } + + @override + void visitNativeClause(NativeClause node) { + _runSubscriptions(node, _registry._forNativeClause); + super.visitNativeClause(node); + } + + @override + void visitNativeFunctionBody(NativeFunctionBody node) { + _runSubscriptions(node, _registry._forNativeFunctionBody); + super.visitNativeFunctionBody(node); + } + + @override + void visitNode(AstNode node) { + _runSubscriptions(node, _registry._forAstNode); + super.visitNode(node); + } + + @override + void visitNormalFormalParameter(NormalFormalParameter node) { + _runSubscriptions(node, _registry._forNormalFormalParameter); + super.visitNormalFormalParameter(node); + } + + @override + void visitNullAssertPattern(NullAssertPattern node) { + _runSubscriptions(node, _registry._forNullAssertPattern); + super.visitNullAssertPattern(node); + } + + @override + void visitNullAwareElement(NullAwareElement node) { + _runSubscriptions(node, _registry._forNullAwareElement); + super.visitNullAwareElement(node); + } + + @override + void visitNullCheckPattern(NullCheckPattern node) { + _runSubscriptions(node, _registry._forNullCheckPattern); + super.visitNullCheckPattern(node); + } + + @override + void visitNullLiteral(NullLiteral node) { + _runSubscriptions(node, _registry._forNullLiteral); + super.visitNullLiteral(node); + } + + @override + void visitObjectPattern(ObjectPattern node) { + _runSubscriptions(node, _registry._forObjectPattern); + super.visitObjectPattern(node); + } + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + _runSubscriptions(node, _registry._forParenthesizedExpression); + super.visitParenthesizedExpression(node); + } + + @override + void visitParenthesizedPattern(ParenthesizedPattern node) { + _runSubscriptions(node, _registry._forParenthesizedPattern); + super.visitParenthesizedPattern(node); + } + + @override + void visitPartDirective(PartDirective node) { + _runSubscriptions(node, _registry._forPartDirective); + super.visitPartDirective(node); + } + + @override + void visitPartOfDirective(PartOfDirective node) { + _runSubscriptions(node, _registry._forPartOfDirective); + super.visitPartOfDirective(node); + } + + @override + void visitPatternAssignment(PatternAssignment node) { + _runSubscriptions(node, _registry._forPatternAssignment); + super.visitPatternAssignment(node); + } + + @override + void visitPatternField(PatternField node) { + _runSubscriptions(node, _registry._forPatternField); + super.visitPatternField(node); + } + + @override + void visitPatternFieldName(PatternFieldName node) { + _runSubscriptions(node, _registry._forPatternFieldName); + super.visitPatternFieldName(node); + } + + @override + void visitPatternVariableDeclaration(PatternVariableDeclaration node) { + _runSubscriptions(node, _registry._forPatternVariableDeclaration); + super.visitPatternVariableDeclaration(node); + } + + @override + void visitPatternVariableDeclarationStatement( + PatternVariableDeclarationStatement node) { + _runSubscriptions(node, _registry._forPatternVariableDeclarationStatement); + super.visitPatternVariableDeclarationStatement(node); + } + + @override + void visitPostfixExpression(PostfixExpression node) { + _runSubscriptions(node, _registry._forPostfixExpression); + super.visitPostfixExpression(node); + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + _runSubscriptions(node, _registry._forPrefixedIdentifier); + super.visitPrefixedIdentifier(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + _runSubscriptions(node, _registry._forPrefixExpression); + super.visitPrefixExpression(node); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + _runSubscriptions(node, _registry._forPropertyAccess); + super.visitPropertyAccess(node); + } + + @override + void visitRecordLiteral(RecordLiteral node) { + _runSubscriptions(node, _registry._forRecordLiteral); + super.visitRecordLiteral(node); + } + + @override + void visitRecordPattern(RecordPattern node) { + _runSubscriptions(node, _registry._forRecordPattern); + super.visitRecordPattern(node); + } + + @override + void visitRecordTypeAnnotation(RecordTypeAnnotation node) { + _runSubscriptions(node, _registry._forRecordTypeAnnotation); + super.visitRecordTypeAnnotation(node); + } + + @override + void visitRecordTypeAnnotationField(RecordTypeAnnotationField node) { + _runSubscriptions(node, _registry._forRecordTypeAnnotationField); + super.visitRecordTypeAnnotationField(node); + } + + @override + void visitRecordTypeAnnotationNamedField( + RecordTypeAnnotationNamedField node) { + _runSubscriptions(node, _registry._forRecordTypeAnnotationNamedField); + super.visitRecordTypeAnnotationNamedField(node); + } + + @override + void visitRecordTypeAnnotationNamedFields( + RecordTypeAnnotationNamedFields node) { + _runSubscriptions(node, _registry._forRecordTypeAnnotationNamedFields); + super.visitRecordTypeAnnotationNamedFields(node); + } + + @override + void visitRecordTypeAnnotationPositionalField( + RecordTypeAnnotationPositionalField node) { + _runSubscriptions(node, _registry._forRecordTypeAnnotationPositionalField); + super.visitRecordTypeAnnotationPositionalField(node); + } + + @override + void visitRedirectingConstructorInvocation( + RedirectingConstructorInvocation node) { + _runSubscriptions(node, _registry._forRedirectingConstructorInvocation); + super.visitRedirectingConstructorInvocation(node); + } + + @override + void visitRelationalPattern(RelationalPattern node) { + _runSubscriptions(node, _registry._forRelationalPattern); + super.visitRelationalPattern(node); + } + + @override + void visitRepresentationConstructorName(RepresentationConstructorName node) { + _runSubscriptions(node, _registry._forRepresentationConstructorName); + super.visitRepresentationConstructorName(node); + } + + @override + void visitRepresentationDeclaration(RepresentationDeclaration node) { + _runSubscriptions(node, _registry._forRepresentationDeclaration); + super.visitRepresentationDeclaration(node); + } + + @override + void visitRestPatternElement(RestPatternElement node) { + _runSubscriptions(node, _registry._forRestPatternElement); + super.visitRestPatternElement(node); + } + + @override + void visitRethrowExpression(RethrowExpression node) { + _runSubscriptions(node, _registry._forRethrowExpression); + super.visitRethrowExpression(node); + } + + @override + void visitReturnStatement(ReturnStatement node) { + _runSubscriptions(node, _registry._forReturnStatement); + super.visitReturnStatement(node); + } + + @override + void visitScriptTag(ScriptTag node) { + _runSubscriptions(node, _registry._forScriptTag); + super.visitScriptTag(node); + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + _runSubscriptions(node, _registry._forSetOrMapLiteral); + super.visitSetOrMapLiteral(node); + } + + @override + void visitShowCombinator(ShowCombinator node) { + _runSubscriptions(node, _registry._forShowCombinator); + super.visitShowCombinator(node); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter node) { + _runSubscriptions(node, _registry._forSimpleFormalParameter); + super.visitSimpleFormalParameter(node); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + _runSubscriptions(node, _registry._forSimpleIdentifier); + super.visitSimpleIdentifier(node); + } + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + _runSubscriptions(node, _registry._forSimpleStringLiteral); + super.visitSimpleStringLiteral(node); + } + + @override + void visitSingleStringLiteral(SingleStringLiteral node) { + _runSubscriptions(node, _registry._forSingleStringLiteral); + super.visitSingleStringLiteral(node); + } + + @override + void visitSpreadElement(SpreadElement node) { + _runSubscriptions(node, _registry._forSpreadElement); + super.visitSpreadElement(node); + } + + @override + void visitStatement(Statement node) { + _runSubscriptions(node, _registry._forStatement); + super.visitStatement(node); + } + + @override + void visitStringInterpolation(StringInterpolation node) { + _runSubscriptions(node, _registry._forStringInterpolation); + super.visitStringInterpolation(node); + } + + @override + void visitStringLiteral(StringLiteral node) { + _runSubscriptions(node, _registry._forStringLiteral); + super.visitStringLiteral(node); + } + + @override + void visitSuperConstructorInvocation(SuperConstructorInvocation node) { + _runSubscriptions(node, _registry._forSuperConstructorInvocation); + super.visitSuperConstructorInvocation(node); + } + + @override + void visitSuperExpression(SuperExpression node) { + _runSubscriptions(node, _registry._forSuperExpression); + super.visitSuperExpression(node); + } + + @override + void visitSuperFormalParameter(SuperFormalParameter node) { + _runSubscriptions(node, _registry._forSuperFormalParameter); + super.visitSuperFormalParameter(node); + } + + @override + void visitSwitchCase(SwitchCase node) { + _runSubscriptions(node, _registry._forSwitchCase); + super.visitSwitchCase(node); + } + + @override + void visitSwitchDefault(SwitchDefault node) { + _runSubscriptions(node, _registry._forSwitchDefault); + super.visitSwitchDefault(node); + } + + @override + void visitSwitchExpression(SwitchExpression node) { + _runSubscriptions(node, _registry._forSwitchExpression); + super.visitSwitchExpression(node); + } + + @override + void visitSwitchExpressionCase(SwitchExpressionCase node) { + _runSubscriptions(node, _registry._forSwitchExpressionCase); + super.visitSwitchExpressionCase(node); + } + + @override + void visitSwitchMember(SwitchMember node) { + _runSubscriptions(node, _registry._forSwitchMember); + super.visitSwitchMember(node); + } + + @override + void visitSwitchPatternCase(SwitchPatternCase node) { + _runSubscriptions(node, _registry._forSwitchPatternCase); + super.visitSwitchPatternCase(node); + } + + @override + void visitSwitchStatement(SwitchStatement node) { + _runSubscriptions(node, _registry._forSwitchStatement); + super.visitSwitchStatement(node); + } + + @override + void visitSymbolLiteral(SymbolLiteral node) { + _runSubscriptions(node, _registry._forSymbolLiteral); + super.visitSymbolLiteral(node); + } + + @override + void visitThisExpression(ThisExpression node) { + _runSubscriptions(node, _registry._forThisExpression); + super.visitThisExpression(node); + } + + @override + void visitThrowExpression(ThrowExpression node) { + _runSubscriptions(node, _registry._forThrowExpression); + super.visitThrowExpression(node); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + _runSubscriptions(node, _registry._forTopLevelVariableDeclaration); + super.visitTopLevelVariableDeclaration(node); + } + + @override + void visitTryStatement(TryStatement node) { + _runSubscriptions(node, _registry._forTryStatement); + super.visitTryStatement(node); + } + + @override + void visitTypeAlias(TypeAlias node) { + _runSubscriptions(node, _registry._forTypeAlias); + super.visitTypeAlias(node); + } + + @override + void visitTypeAnnotation(TypeAnnotation node) { + _runSubscriptions(node, _registry._forTypeAnnotation); + super.visitTypeAnnotation(node); + } + + @override + void visitTypeArgumentList(TypeArgumentList node) { + _runSubscriptions(node, _registry._forTypeArgumentList); + super.visitTypeArgumentList(node); + } + + @override + void visitTypedLiteral(TypedLiteral node) { + _runSubscriptions(node, _registry._forTypedLiteral); + super.visitTypedLiteral(node); + } + + @override + void visitTypeLiteral(TypeLiteral node) { + _runSubscriptions(node, _registry._forTypeLiteral); + super.visitTypeLiteral(node); + } + + @override + void visitTypeParameter(TypeParameter node) { + _runSubscriptions(node, _registry._forTypeParameter); + super.visitTypeParameter(node); + } + + @override + void visitTypeParameterList(TypeParameterList node) { + _runSubscriptions(node, _registry._forTypeParameterList); + super.visitTypeParameterList(node); + } + + @override + void visitUriBasedDirective(UriBasedDirective node) { + _runSubscriptions(node, _registry._forUriBasedDirective); + super.visitUriBasedDirective(node); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + _runSubscriptions(node, _registry._forVariableDeclaration); + super.visitVariableDeclaration(node); + } + + @override + void visitVariableDeclarationList(VariableDeclarationList node) { + _runSubscriptions(node, _registry._forVariableDeclarationList); + super.visitVariableDeclarationList(node); + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + _runSubscriptions(node, _registry._forVariableDeclarationStatement); + super.visitVariableDeclarationStatement(node); + } + + @override + void visitWhenClause(WhenClause node) { + _runSubscriptions(node, _registry._forWhenClause); + super.visitWhenClause(node); + } + + @override + void visitWhileStatement(WhileStatement node) { + _runSubscriptions(node, _registry._forWhileStatement); + super.visitWhileStatement(node); + } + + @override + void visitWildcardPattern(WildcardPattern node) { + _runSubscriptions(node, _registry._forWildcardPattern); + super.visitWildcardPattern(node); + } + + @override + void visitWithClause(WithClause node) { + _runSubscriptions(node, _registry._forWithClause); + super.visitWithClause(node); + } + + @override + void visitYieldStatement(YieldStatement node) { + _runSubscriptions(node, _registry._forYieldStatement); + super.visitYieldStatement(node); + } +} + +/// A single subscription for a node type, by the specified "key" +class _Subscription { + _Subscription(this.listener, this.timer, this.zone); + + final void Function(T node) listener; + final Stopwatch? timer; + final Zone zone; +} + +/// The container to register visitors for separate AST node types. +class NodeLintRegistry { + /// The container to register visitors for separate AST node types. + NodeLintRegistry(this._lintRegistry, {required bool enableTiming}) + : _enableTiming = enableTiming; + + final LintRegistry _lintRegistry; + final bool _enableTiming; + + /// Get the timer associated with the given [key]. + Stopwatch? _getTimer(String key) { + if (_enableTiming) { + return _lintRegistry.getTimer(key); + } else { + return null; + } + } + + final List<_Subscription> _forAdjacentStrings = []; + void addAdjacentStrings( + String key, void Function(AdjacentStrings node) listener) { + _forAdjacentStrings + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAnnotatedNode = []; + void addAnnotatedNode( + String key, void Function(AnnotatedNode node) listener) { + _forAnnotatedNode + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAnnotation = []; + void addAnnotation(String key, void Function(Annotation node) listener) { + _forAnnotation.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forArgumentList = []; + void addArgumentList(String key, void Function(ArgumentList node) listener) { + _forArgumentList.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAsExpression = []; + void addAsExpression(String key, void Function(AsExpression node) listener) { + _forAsExpression.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAssertInitializer = []; + void addAssertInitializer( + String key, void Function(AssertInitializer node) listener) { + _forAssertInitializer + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAssertStatement = []; + void addAssertStatement( + String key, void Function(AssertStatement node) listener) { + _forAssertStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forAssignedVariablePattern = []; + void addAssignedVariablePattern( + String key, void Function(AssignedVariablePattern node) listener) { + _forAssignedVariablePattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAssignmentExpression = []; + void addAssignmentExpression( + String key, void Function(AssignmentExpression node) listener) { + _forAssignmentExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAugmentedExpression = []; + void addAugmentedExpression( + String key, void Function(AugmentedExpression node) listener) { + _forAugmentedExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAugmentedInvocation = []; + void addAugmentedInvocation( + String key, void Function(AugmentedInvocation node) listener) { + _forAugmentedInvocation + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forAwaitExpression = []; + void addAwaitExpression( + String key, void Function(AwaitExpression node) listener) { + _forAwaitExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forBinaryExpression = []; + void addBinaryExpression( + String key, void Function(BinaryExpression node) listener) { + _forBinaryExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forBlock = []; + void addBlock(String key, void Function(Block node) listener) { + _forBlock.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forBlockFunctionBody = []; + void addBlockFunctionBody( + String key, void Function(BlockFunctionBody node) listener) { + _forBlockFunctionBody + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forBooleanLiteral = []; + void addBooleanLiteral( + String key, void Function(BooleanLiteral node) listener) { + _forBooleanLiteral + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forBreakStatement = []; + void addBreakStatement( + String key, void Function(BreakStatement node) listener) { + _forBreakStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCascadeExpression = []; + void addCascadeExpression( + String key, void Function(CascadeExpression node) listener) { + _forCascadeExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCaseClause = []; + void addCaseClause(String key, void Function(CaseClause node) listener) { + _forCaseClause.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCastPattern = []; + void addCastPattern(String key, void Function(CastPattern node) listener) { + _forCastPattern.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCatchClause = []; + void addCatchClause(String key, void Function(CatchClause node) listener) { + _forCatchClause.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCatchClauseParameter = []; + void addCatchClauseParameter( + String key, void Function(CatchClauseParameter node) listener) { + _forCatchClauseParameter + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forClassDeclaration = []; + void addClassDeclaration( + String key, void Function(ClassDeclaration node) listener) { + _forClassDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forClassMember = []; + void addClassMember(String key, void Function(ClassMember node) listener) { + _forClassMember.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forClassTypeAlias = []; + void addClassTypeAlias( + String key, void Function(ClassTypeAlias node) listener) { + _forClassTypeAlias + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCollectionElement = []; + void addCollectionElement( + String key, void Function(CollectionElement node) listener) { + _forCollectionElement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCombinator = []; + void addCombinator(String key, void Function(Combinator node) listener) { + _forCombinator.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forComment = []; + void addComment(String key, void Function(Comment node) listener) { + _forComment.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCommentReference = []; + void addCommentReference( + String key, void Function(CommentReference node) listener) { + _forCommentReference + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCompilationUnit = []; + void addCompilationUnit( + String key, void Function(CompilationUnit node) listener) { + _forCompilationUnit + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forCompilationUnitMember = + []; + void addCompilationUnitMember( + String key, void Function(CompilationUnitMember node) listener) { + _forCompilationUnitMember + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConditionalExpression = + []; + void addConditionalExpression( + String key, void Function(ConditionalExpression node) listener) { + _forConditionalExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConfiguration = []; + void addConfiguration( + String key, void Function(Configuration node) listener) { + _forConfiguration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstantPattern = []; + void addConstantPattern( + String key, void Function(ConstantPattern node) listener) { + _forConstantPattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstructorDeclaration = + []; + void addConstructorDeclaration( + String key, void Function(ConstructorDeclaration node) listener) { + _forConstructorDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forConstructorFieldInitializer = []; + void addConstructorFieldInitializer( + String key, void Function(ConstructorFieldInitializer node) listener) { + _forConstructorFieldInitializer + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstructorInitializer = + []; + void addConstructorInitializer( + String key, void Function(ConstructorInitializer node) listener) { + _forConstructorInitializer + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstructorName = []; + void addConstructorName( + String key, void Function(ConstructorName node) listener) { + _forConstructorName + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstructorReference = []; + void addConstructorReference( + String key, void Function(ConstructorReference node) listener) { + _forConstructorReference + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forConstructorSelector = []; + void addConstructorSelector( + String key, void Function(ConstructorSelector node) listener) { + _forConstructorSelector + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forContinueStatement = []; + void addContinueStatement( + String key, void Function(ContinueStatement node) listener) { + _forContinueStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDartPattern = []; + void addDartPattern(String key, void Function(DartPattern node) listener) { + _forDartPattern.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDeclaration = []; + void addDeclaration(String key, void Function(Declaration node) listener) { + _forDeclaration.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDeclaredIdentifier = []; + void addDeclaredIdentifier( + String key, void Function(DeclaredIdentifier node) listener) { + _forDeclaredIdentifier + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forDeclaredVariablePattern = []; + void addDeclaredVariablePattern( + String key, void Function(DeclaredVariablePattern node) listener) { + _forDeclaredVariablePattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDefaultFormalParameter = + []; + void addDefaultFormalParameter( + String key, void Function(DefaultFormalParameter node) listener) { + _forDefaultFormalParameter + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDirective = []; + void addDirective(String key, void Function(Directive node) listener) { + _forDirective.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDoStatement = []; + void addDoStatement(String key, void Function(DoStatement node) listener) { + _forDoStatement.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDottedName = []; + void addDottedName(String key, void Function(DottedName node) listener) { + _forDottedName.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forDoubleLiteral = []; + void addDoubleLiteral( + String key, void Function(DoubleLiteral node) listener) { + _forDoubleLiteral + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forEmptyFunctionBody = []; + void addEmptyFunctionBody( + String key, void Function(EmptyFunctionBody node) listener) { + _forEmptyFunctionBody + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forEmptyStatement = []; + void addEmptyStatement( + String key, void Function(EmptyStatement node) listener) { + _forEmptyStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forEnumConstantArguments = + []; + void addEnumConstantArguments( + String key, void Function(EnumConstantArguments node) listener) { + _forEnumConstantArguments + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forEnumConstantDeclaration = []; + void addEnumConstantDeclaration( + String key, void Function(EnumConstantDeclaration node) listener) { + _forEnumConstantDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forEnumDeclaration = []; + void addEnumDeclaration( + String key, void Function(EnumDeclaration node) listener) { + _forEnumDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExportDirective = []; + void addExportDirective( + String key, void Function(ExportDirective node) listener) { + _forExportDirective + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExpression = []; + void addExpression(String key, void Function(Expression node) listener) { + _forExpression.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExpressionFunctionBody = + []; + void addExpressionFunctionBody( + String key, void Function(ExpressionFunctionBody node) listener) { + _forExpressionFunctionBody + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExpressionStatement = []; + void addExpressionStatement( + String key, void Function(ExpressionStatement node) listener) { + _forExpressionStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExtendsClause = []; + void addExtendsClause( + String key, void Function(ExtendsClause node) listener) { + _forExtendsClause + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExtensionDeclaration = []; + void addExtensionDeclaration( + String key, void Function(ExtensionDeclaration node) listener) { + _forExtensionDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExtensionOnClause = []; + void addExtensionOnClause( + String key, void Function(ExtensionOnClause node) listener) { + _forExtensionOnClause + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forExtensionOverride = []; + void addExtensionOverride( + String key, void Function(ExtensionOverride node) listener) { + _forExtensionOverride + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forExtensionTypeDeclaration = []; + void addExtensionTypeDeclaration( + String key, void Function(ExtensionTypeDeclaration node) listener) { + _forExtensionTypeDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFieldDeclaration = []; + void addFieldDeclaration( + String key, void Function(FieldDeclaration node) listener) { + _forFieldDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFieldFormalParameter = []; + void addFieldFormalParameter( + String key, void Function(FieldFormalParameter node) listener) { + _forFieldFormalParameter + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForEachParts = []; + void addForEachParts(String key, void Function(ForEachParts node) listener) { + _forForEachParts.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forForEachPartsWithDeclaration = []; + void addForEachPartsWithDeclaration( + String key, void Function(ForEachPartsWithDeclaration node) listener) { + _forForEachPartsWithDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forForEachPartsWithIdentifier = []; + void addForEachPartsWithIdentifier( + String key, void Function(ForEachPartsWithIdentifier node) listener) { + _forForEachPartsWithIdentifier + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forForEachPartsWithPattern = []; + void addForEachPartsWithPattern( + String key, void Function(ForEachPartsWithPattern node) listener) { + _forForEachPartsWithPattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForElement = []; + void addForElement(String key, void Function(ForElement node) listener) { + _forForElement.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFormalParameter = []; + void addFormalParameter( + String key, void Function(FormalParameter node) listener) { + _forFormalParameter + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFormalParameterList = []; + void addFormalParameterList( + String key, void Function(FormalParameterList node) listener) { + _forFormalParameterList + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForParts = []; + void addForParts(String key, void Function(ForParts node) listener) { + _forForParts.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forForPartsWithDeclarations = []; + void addForPartsWithDeclarations( + String key, void Function(ForPartsWithDeclarations node) listener) { + _forForPartsWithDeclarations + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForPartsWithExpression = + []; + void addForPartsWithExpression( + String key, void Function(ForPartsWithExpression node) listener) { + _forForPartsWithExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForPartsWithPattern = []; + void addForPartsWithPattern( + String key, void Function(ForPartsWithPattern node) listener) { + _forForPartsWithPattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forForStatement = []; + void addForStatement(String key, void Function(ForStatement node) listener) { + _forForStatement.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFunctionBody = []; + void addFunctionBody(String key, void Function(FunctionBody node) listener) { + _forFunctionBody.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFunctionDeclaration = []; + void addFunctionDeclaration( + String key, void Function(FunctionDeclaration node) listener) { + _forFunctionDeclaration + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forFunctionDeclarationStatement = []; + void addFunctionDeclarationStatement( + String key, void Function(FunctionDeclarationStatement node) listener) { + _forFunctionDeclarationStatement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFunctionExpression = []; + void addFunctionExpression( + String key, void Function(FunctionExpression node) listener) { + _forFunctionExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forFunctionExpressionInvocation = []; + void addFunctionExpressionInvocation( + String key, void Function(FunctionExpressionInvocation node) listener) { + _forFunctionExpressionInvocation + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFunctionReference = []; + void addFunctionReference( + String key, void Function(FunctionReference node) listener) { + _forFunctionReference + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forFunctionTypeAlias = []; + void addFunctionTypeAlias( + String key, void Function(FunctionTypeAlias node) listener) { + _forFunctionTypeAlias + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forFunctionTypedFormalParameter = []; + void addFunctionTypedFormalParameter( + String key, void Function(FunctionTypedFormalParameter node) listener) { + _forFunctionTypedFormalParameter + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forGenericFunctionType = []; + void addGenericFunctionType( + String key, void Function(GenericFunctionType node) listener) { + _forGenericFunctionType + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forGenericTypeAlias = []; + void addGenericTypeAlias( + String key, void Function(GenericTypeAlias node) listener) { + _forGenericTypeAlias + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forGuardedPattern = []; + void addGuardedPattern( + String key, void Function(GuardedPattern node) listener) { + _forGuardedPattern + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forHideCombinator = []; + void addHideCombinator( + String key, void Function(HideCombinator node) listener) { + _forHideCombinator + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIdentifier = []; + void addIdentifier(String key, void Function(Identifier node) listener) { + _forIdentifier.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIfElement = []; + void addIfElement(String key, void Function(IfElement node) listener) { + _forIfElement.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIfStatement = []; + void addIfStatement(String key, void Function(IfStatement node) listener) { + _forIfStatement.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forImplementsClause = []; + void addImplementsClause( + String key, void Function(ImplementsClause node) listener) { + _forImplementsClause + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forImplicitCallReference = + []; + void addImplicitCallReference( + String key, void Function(ImplicitCallReference node) listener) { + _forImplicitCallReference + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forImportDirective = []; + void addImportDirective( + String key, void Function(ImportDirective node) listener) { + _forImportDirective + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forImportPrefixReference = + []; + void addImportPrefixReference( + String key, void Function(ImportPrefixReference node) listener) { + _forImportPrefixReference + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIndexExpression = []; + void addIndexExpression( + String key, void Function(IndexExpression node) listener) { + _forIndexExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forInstanceCreationExpression = []; + void addInstanceCreationExpression( + String key, void Function(InstanceCreationExpression node) listener) { + _forInstanceCreationExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIntegerLiteral = []; + void addIntegerLiteral( + String key, void Function(IntegerLiteral node) listener) { + _forIntegerLiteral + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forInterpolationElement = []; + void addInterpolationElement( + String key, void Function(InterpolationElement node) listener) { + _forInterpolationElement + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> + _forInterpolationExpression = []; + void addInterpolationExpression( + String key, void Function(InterpolationExpression node) listener) { + _forInterpolationExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forInterpolationString = []; + void addInterpolationString( + String key, void Function(InterpolationString node) listener) { + _forInterpolationString + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forInvocationExpression = []; + void addInvocationExpression( + String key, void Function(InvocationExpression node) listener) { + _forInvocationExpression + .add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription> _forIsExpression = []; + void addIsExpression(String key, void Function(IsExpression node) listener) { + _forIsExpression.add(_Subscription(listener, _getTimer(key), Zone.current)); + } + + final List<_Subscription