├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── example_request.md │ └── feature_request.md ├── dependabot.yaml └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── ROADMAP.md ├── all_lint_rules.yaml ├── analysis_options.yaml ├── docs ├── assists.md ├── fixes.md └── lints.md ├── melos.yaml ├── packages ├── custom_lint │ ├── .pubignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── custom_lint.dart │ ├── build.yaml │ ├── example │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── example.md │ │ ├── example_lint │ │ │ ├── analysis_options.yaml │ │ │ ├── lib │ │ │ │ └── custom_lint_example_lint.dart │ │ │ ├── pubspec.yaml │ │ │ └── pubspec_overrides.yaml │ │ ├── lib │ │ │ ├── main.dart │ │ │ └── test.jpeg │ │ ├── pubspec.yaml │ │ └── pubspec_overrides.yaml │ ├── lib │ │ ├── basic_runner.dart │ │ ├── custom_lint.dart │ │ └── src │ │ │ ├── analyzer_plugin_starter.dart │ │ │ ├── analyzer_utils │ │ │ └── analyzer_utils.dart │ │ │ ├── async_operation.dart │ │ │ ├── channels.dart │ │ │ ├── cli_logger.dart │ │ │ ├── client_isolate_channel.dart │ │ │ ├── output │ │ │ ├── default_output_format.dart │ │ │ ├── json_output_format.dart │ │ │ ├── output_format.dart │ │ │ └── render_lints.dart │ │ │ ├── plugin_delegate.dart │ │ │ ├── request_extension.dart │ │ │ ├── runner.dart │ │ │ ├── server_isolate_channel.dart │ │ │ ├── v2 │ │ │ ├── custom_lint_analyzer_plugin.dart │ │ │ ├── protocol.dart │ │ │ ├── protocol.freezed.dart │ │ │ ├── protocol.g.dart │ │ │ └── server_to_client_channel.dart │ │ │ └── workspace.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ ├── test │ │ ├── cli_process_test.dart │ │ ├── cli_test.dart │ │ ├── create_project.dart │ │ ├── equals_ignoring_ansi.dart │ │ ├── error_report_test.dart │ │ ├── expect_lint_test.dart │ │ ├── fixes_test.dart │ │ ├── goldens.dart │ │ ├── goldens │ │ │ ├── fixes │ │ │ │ ├── add_ignore.diff │ │ │ │ ├── fixes.diff │ │ │ │ ├── multi_change.diff │ │ │ │ ├── no_change.diff │ │ │ │ ├── silenced_change.diff │ │ │ │ ├── single_fix.diff │ │ │ │ └── update_ignore.diff │ │ │ ├── ignore_quick_fix.json │ │ │ └── server_test │ │ │ │ └── redirect_logs.golden │ │ ├── ignore_test.dart │ │ ├── matchers.dart │ │ ├── mock_fs.dart │ │ ├── peer_project_meta.dart │ │ ├── run_plugin.dart │ │ ├── server_test.dart │ │ └── src │ │ │ └── workspace_test.dart │ └── tools │ │ └── analyzer_plugin │ │ ├── bin │ │ └── plugin.dart │ │ └── pubspec.yaml ├── custom_lint_builder │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── example.md │ │ ├── example_lint │ │ │ ├── analysis_options.yaml │ │ │ ├── lib │ │ │ │ └── custom_lint_builder_example_lint.dart │ │ │ ├── pubspec.yaml │ │ │ └── pubspec_overrides.yaml │ │ ├── lib │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ └── pubspec_overrides.yaml │ ├── lib │ │ ├── custom_lint_builder.dart │ │ └── src │ │ │ ├── channel.dart │ │ │ ├── client.dart │ │ │ ├── custom_analyzer_converter.dart │ │ │ ├── expect_lint.dart │ │ │ ├── ignore.dart │ │ │ └── pragrams.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test │ │ └── analyzer_converter_test.dart ├── custom_lint_core │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build.yaml │ ├── lib │ │ ├── custom_lint_core.dart │ │ └── src │ │ │ ├── assist.dart │ │ │ ├── change_reporter.dart │ │ │ ├── configs.dart │ │ │ ├── fixes.dart │ │ │ ├── lint_codes.dart │ │ │ ├── lint_rule.dart │ │ │ ├── matcher.dart │ │ │ ├── package_utils.dart │ │ │ ├── plugin_base.dart │ │ │ ├── pragmas.dart │ │ │ ├── resolver.dart │ │ │ ├── runnable.dart │ │ │ ├── source_range_extensions.dart │ │ │ └── type_checker.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test │ │ ├── assist_test.dart │ │ ├── configs_test.dart │ │ ├── fix_test.dart │ │ ├── lint_rule_test.dart │ │ ├── snapshot.diff │ │ ├── snapshot2.diff │ │ └── type_checker_test.dart ├── custom_lint_visitor │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build.yaml │ ├── lib │ │ ├── custom_lint_visitor.dart │ │ └── src │ │ │ ├── node_lint_visitor.dart │ │ │ ├── node_lint_visitor.g.dart │ │ │ └── pragmas.dart │ ├── pubspec.yaml │ └── pubspec_overrides.yaml └── lint_visitor_generator │ ├── LICENSE │ ├── README.md │ ├── build.yaml │ ├── lib │ └── builder.dart │ └── pubspec.yaml ├── pubspec.yaml ├── resources └── lint_showcase.png └── website ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── astro.config.mjs ├── package-lock.json ├── package.json ├── public ├── favicons │ └── favicon.svg └── images │ ├── bg_header.svg │ ├── bg_header_corner.svg │ ├── bg_header_mobile.svg │ ├── bg_plugins_shadow.svg │ ├── bg_shadow_l_b.svg │ ├── bg_shadow_l_t.svg │ ├── chart_dark.svg │ ├── cover.jpg │ ├── lines_left.svg │ ├── lines_right.svg │ ├── lint_showcase.png │ ├── particles_wave.webp │ ├── vs_code_dark.svg │ ├── vs_code_shadow_dark.svg │ └── vscode_example.jpg ├── src ├── components │ ├── Card.astro │ ├── Footer.astro │ ├── Header.astro │ ├── Logo.astro │ ├── MobileMenu.astro │ ├── PluginCodeExample.astro │ ├── Toggle.astro │ ├── VsCodeBanner.astro │ └── icons │ │ ├── IconChevronUp.astro │ │ ├── IconExternalLink.astro │ │ ├── IconHamburger.astro │ │ ├── IconMoon.astro │ │ └── IconSun.astro ├── env.d.ts ├── fonts │ ├── ITCAvantGardePro-Bk.woff │ ├── ITCAvantGardePro-Demi.woff │ ├── ITCAvantGardePro-Md.woff │ └── ITCAvantGardeRegular.woff ├── layouts │ └── Root.astro ├── nav.ts ├── pages │ └── index.astro └── styles │ ├── fonts.css │ └── index.css ├── tailwind.config.cjs └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: There is a problem in how provider behaves 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a problem and I need help 4 | url: https://github.com/rrousselGit/riverpod/discussions 5 | about: Pleast ask and answer questions here 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/example_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation improvement request 3 | about: >- 4 | Suggest a new example/documentation or ask for clarification about an 5 | existing one. 6 | title: "" 7 | labels: documentation, needs triage 8 | assignees: 9 | - rrousselGit 10 | --- 11 | 12 | **Describe what scenario you think is uncovered by the existing examples/articles** 13 | A clear and concise description of the problem that you want explained. 14 | 15 | **Describe why existing examples/articles do not cover this case** 16 | Explain which examples/articles you have seen before making this request, and 17 | why they did not help you with your problem. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the documentation request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement, needs triage 6 | assignees: 7 | - rrousselGit 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "pub" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "**.md" 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - "**.md" 12 | schedule: 13 | # runs the CI everyday at 10AM 14 | - cron: "0 10 * * *" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | channel: 23 | - master 24 | pub: 25 | - get 26 | - upgrade 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - uses: subosito/flutter-action@v2 32 | with: 33 | channel: ${{ matrix.channel }} 34 | 35 | - name: Add pub cache bin to PATH 36 | run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH 37 | 38 | - name: Add pub cache to PATH 39 | run: echo "PUB_CACHE="$HOME/.pub-cache"" >> $GITHUB_ENV 40 | 41 | - name: Add pubspec_overrides to the analyzer_plugin starter 42 | run: "echo \"dependency_overrides:\n custom_lint:\n path: ${{github.workspace}}/packages/custom_lint\" > packages/custom_lint/tools/analyzer_plugin/pubspec_overrides.yaml" 43 | 44 | - run: dart pub global activate melos 45 | 46 | - name: Install dependencies 47 | run: melos exec -- "dart pub ${{ matrix.pub }}" 48 | 49 | - name: Check format 50 | run: dart format --set-exit-if-changed . 51 | 52 | - name: Analyze 53 | run: dart analyze 54 | 55 | - name: Run tests 56 | run: melos exec --dir-exists=test "dart test" 57 | 58 | # - name: Upload coverage to codecov 59 | # run: curl -s https://codecov.io/bash | bash 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log.txt 2 | 3 | pubspec.lock 4 | .packages 5 | 6 | # Ignoring generated files, as they pollute pull requests and can create merge conflicts 7 | *.g.dart 8 | *.freezed.dart 9 | # Including generated files from the lib folders, as these should be published on pub 10 | !packages/*/lib/**/*.freezed.dart 11 | !packages/*/lib/**/*.g.dart 12 | 13 | # Ignoring native folders of the example as they can be re-generated easily 14 | **/example/android/ 15 | **/example/ios/ 16 | **/example/web/ 17 | examples/**/android/ 18 | examples/**/ios/ 19 | examples/**/web/ 20 | examples/**/macos/ 21 | 22 | # Miscellaneous 23 | coverage/ 24 | *.class 25 | *.log 26 | *.pyc 27 | *.swp 28 | .DS_Store 29 | .atom/ 30 | .buildlog/ 31 | .history 32 | .svn/ 33 | coverage.lcov 34 | 35 | # IntelliJ related 36 | *.iml 37 | *.ipr 38 | *.iws 39 | .idea/ 40 | 41 | # The .vscode folder contains launch configuration and tasks you configure in 42 | # VS Code which you may wish to be included in version control, so this line 43 | # is commented out by default. 44 | #.vscode/ 45 | 46 | # Flutter/Dart/Pub related 47 | **/doc/api/ 48 | .dart_tool/ 49 | .flutter-plugins 50 | .flutter-plugins-dependencies 51 | .packages 52 | .pub-cache/ 53 | .pub/ 54 | build/ 55 | 56 | # Web related 57 | lib/generated_plugin_registrant.dart 58 | 59 | # Exceptions to above rules. 60 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 61 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "delete .plugin_manager", 6 | "command": "rm -rf ~/.dartServer/.plugin_manager/", 7 | "type": "shell", 8 | "presentation": { 9 | "reveal": "never", 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ## Add built-in lints for highlighting invalid custom_lint & custom_lint_builder usage 2 | 3 | - no `bin/custom_lint.dart` found 4 | - highlight in the IDE on the pubspec.yaml plugins that failed to start 5 | 6 | ## Add custom_lint_test 7 | 8 | For simplifying testing plugins 9 | 10 | ## Support disabling lint rules inside the analysis_options.yaml 11 | 12 | Such as: 13 | 14 | ```yaml 15 | linter: 16 | rules: 17 | require_trailing_commas: false 18 | 19 | custom_lint: 20 | rules: 21 | riverpod_final_provider: false 22 | ``` 23 | 24 | ## Add support for refactors and fixes 25 | 26 | Instead of being limited to lints 27 | 28 | Bonus point for a `dart run custom_lint --fix` 29 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: all_lint_rules.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | errors: 8 | # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. 9 | # We explicitly enabled even conflicting rules and are fixing the conflict 10 | # in this file 11 | included_file_warning: ignore 12 | # false positive when using Freezed 13 | invalid_annotation_target: ignore 14 | deprecated_member_use: ignore 15 | 16 | linter: 17 | rules: 18 | # false positive 19 | one_member_abstracts: false 20 | 21 | # too verbose 22 | prefer_final_parameters: false 23 | 24 | # Too verbose with little value, and this is taken care of by the Flutter devtool anyway. 25 | diagnostic_describe_all_properties: false 26 | 27 | # Personal preference. I prefer "if (bool) return;" over having it in multiple lines 28 | always_put_control_body_on_new_line: false 29 | 30 | # Personal preference. I don't find it more readable 31 | cascade_invocations: false 32 | 33 | # Conflicts with `prefer_single_quotes` 34 | # Single quotes are easier to type and don't compromise on readability. 35 | prefer_double_quotes: false 36 | 37 | # Conflicts with `omit_local_variable_types` and other rules. 38 | # As per Dart guidelines, we want to avoid unnecessary types to make the code 39 | # more readable. 40 | # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables 41 | always_specify_types: false 42 | 43 | # Incompatible with `prefer_final_locals` 44 | # Having immutable local variables makes larger functions more predictible 45 | # so we will use `prefer_final_locals` instead. 46 | unnecessary_final: false 47 | 48 | # Not quite suitable for Flutter, which may have a `build` method with a single 49 | # return, but that return is still complex enough that a "body" is worth it. 50 | prefer_expression_function_bodies: false 51 | 52 | # Conflicts with the convention used by flutter, which puts `Key key` 53 | # and `@required Widget child` last. 54 | always_put_required_named_parameters_first: false 55 | 56 | # This project doesn't use Flutter-style todos 57 | flutter_style_todos: false 58 | 59 | # There are situations where we voluntarily want to catch everything, 60 | # especially as a library. 61 | avoid_catches_without_on_clauses: false 62 | 63 | # Boring as it sometimes force a line of 81 characters to be split in two. 64 | # As long as we try to respect that 80 characters limit, going slightly 65 | # above is fine. 66 | lines_longer_than_80_chars: false 67 | 68 | # Conflicts with disabling `implicit-dynamic` 69 | avoid_annotating_with_dynamic: false 70 | 71 | # conflicts with `prefer_relative_imports` 72 | always_use_package_imports: false 73 | 74 | # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` 75 | no_default_cases: false 76 | -------------------------------------------------------------------------------- /docs/assists.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/docs/assists.md -------------------------------------------------------------------------------- /docs/fixes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/docs/fixes.md -------------------------------------------------------------------------------- /docs/lints.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/docs/lints.md -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_workspace 2 | 3 | packages: 4 | - packages/custom_lint* 5 | - packages/custom_lint*/example** 6 | - packages/lint_visitor_generator 7 | -------------------------------------------------------------------------------- /packages/custom_lint/.pubignore: -------------------------------------------------------------------------------- 1 | # Ignoring the override file as it is useful for development only 2 | # and require an absolute file path – which is unique to the developer. 3 | tools/analyzer_plugin/pubspec_overrides.yaml 4 | -------------------------------------------------------------------------------- /packages/custom_lint/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /packages/custom_lint/bin/custom_lint.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:custom_lint/custom_lint.dart'; 6 | import 'package:custom_lint/src/output/output_format.dart'; 7 | 8 | Future entrypoint([List args = const []]) async { 9 | final parser = ArgParser() 10 | ..addFlag( 11 | 'fatal-infos', 12 | help: 'Treat info level issues as fatal', 13 | defaultsTo: true, 14 | ) 15 | ..addFlag( 16 | 'fatal-warnings', 17 | help: 'Treat warning level issues as fatal', 18 | defaultsTo: true, 19 | ) 20 | ..addOption( 21 | 'format', 22 | valueHelp: 'value', 23 | help: 'Specifies the format to display lints.', 24 | defaultsTo: 'default', 25 | allowed: [ 26 | OutputFormatEnum.plain.name, 27 | OutputFormatEnum.json.name, 28 | ], 29 | allowedHelp: { 30 | 'default': 31 | 'The default output format. This format is intended to be user ' 32 | 'consumable.\nThe format is not specified and can change ' 33 | 'between releases.', 34 | 'json': 'A machine readable output in a JSON format.', 35 | }, 36 | ) 37 | ..addFlag( 38 | 'watch', 39 | help: "Watches plugins' sources and perform a hot-reload on change", 40 | negatable: false, 41 | ) 42 | ..addFlag( 43 | 'fix', 44 | help: 'Apply all possible fixes to the lint issues found.', 45 | negatable: false, 46 | ) 47 | ..addFlag( 48 | 'help', 49 | abbr: 'h', 50 | negatable: false, 51 | help: 'Prints command usage', 52 | ); 53 | final result = parser.parse(args); 54 | 55 | final help = result['help'] as bool; 56 | if (help) { 57 | stdout.writeln('Usage: custom_lint [--watch]'); 58 | stdout.writeln(parser.usage); 59 | return; 60 | } 61 | 62 | final watchMode = result['watch'] as bool; 63 | final fix = result['fix'] as bool; 64 | final fatalInfos = result['fatal-infos'] as bool; 65 | final fatalWarnings = result['fatal-warnings'] as bool; 66 | final format = result['format'] as String; 67 | 68 | await customLint( 69 | workingDirectory: Directory.current, 70 | watchMode: watchMode, 71 | fatalInfos: fatalInfos, 72 | fatalWarnings: fatalWarnings, 73 | fix: fix, 74 | format: OutputFormatEnum.fromName(format), 75 | ); 76 | } 77 | 78 | void main([List args = const []]) async { 79 | try { 80 | await entrypoint(args); 81 | } finally { 82 | // TODO figure out why this exit is necessary 83 | exit(exitCode); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/custom_lint/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | source_gen|combining_builder: 5 | options: 6 | ignore_for_file: 7 | - "type=lint" 8 | -------------------------------------------------------------------------------- /packages/custom_lint/example/README.md: -------------------------------------------------------------------------------- 1 | # Custom Lint Example 2 | 3 | A simple example how powerful is custom_lint package. 4 | -------------------------------------------------------------------------------- /packages/custom_lint/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../../../analysis_options.yaml 2 | 3 | analyzer: 4 | plugins: 5 | - custom_lint 6 | 7 | linter: 8 | rules: 9 | public_member_api_docs: false 10 | avoid_print: false 11 | unreachable_from_main: false 12 | -------------------------------------------------------------------------------- /packages/custom_lint/example/example.md: -------------------------------------------------------------------------------- 1 | # Custom Lint Example 2 | 3 | A simple example how powerful is custom_lint package. 4 | -------------------------------------------------------------------------------- /packages/custom_lint/example/example_lint/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # include: ../analysis_options.yaml 2 | 3 | # linter: 4 | # rules: 5 | # public_member_api_docs: false 6 | # avoid_print: false 7 | -------------------------------------------------------------------------------- /packages/custom_lint/example/example_lint/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_example_lint 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=3.0.0 <4.0.0' 6 | 7 | dependencies: 8 | analyzer: ^7.0.0 9 | analyzer_plugin: ^0.13.0 10 | custom_lint_builder: 11 | path: ../../../custom_lint_builder 12 | 13 | dev_dependencies: 14 | custom_lint: 15 | -------------------------------------------------------------------------------- /packages/custom_lint/example/example_lint/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: ../.. 5 | custom_lint_builder: 6 | path: ../../../custom_lint_builder 7 | custom_lint_core: 8 | path: ../../../custom_lint_core 9 | -------------------------------------------------------------------------------- /packages/custom_lint/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod/riverpod.dart'; 2 | 3 | void main() { 4 | print('hello world'); 5 | } 6 | 7 | class Main {} 8 | 9 | // expect_lint: riverpod_final_provider 10 | ProviderBase provider = Provider((ref) => 0); 11 | 12 | // expect_lint: riverpod_final_provider 13 | Provider provider2 = Provider((ref) => 0); 14 | 15 | Object? foo = 42; 16 | -------------------------------------------------------------------------------- /packages/custom_lint/example/lib/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/packages/custom_lint/example/lib/test.jpeg -------------------------------------------------------------------------------- /packages/custom_lint/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_example_app 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=3.0.0 <4.0.0' 6 | 7 | dependencies: 8 | riverpod: ^2.0.0 9 | 10 | dev_dependencies: 11 | custom_lint: 12 | custom_lint_example_lint: 13 | path: ./example_lint 14 | -------------------------------------------------------------------------------- /packages/custom_lint/example/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_example_lint,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: .. 5 | custom_lint_builder: 6 | path: ../../custom_lint_builder 7 | custom_lint_core: 8 | path: ../../custom_lint_core 9 | custom_lint_example_lint: 10 | path: example_lint 11 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/basic_runner.dart: -------------------------------------------------------------------------------- 1 | @Deprecated('Import `package:custom_lint/custom_lint.dart` instead') 2 | library; 3 | 4 | export 'custom_lint.dart'; 5 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/custom_lint.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:cli_util/cli_logging.dart'; 6 | 7 | import 'src/cli_logger.dart'; 8 | import 'src/output/output_format.dart'; 9 | import 'src/output/render_lints.dart'; 10 | import 'src/plugin_delegate.dart'; 11 | import 'src/runner.dart'; 12 | import 'src/server_isolate_channel.dart'; 13 | import 'src/v2/custom_lint_analyzer_plugin.dart'; 14 | import 'src/workspace.dart'; 15 | 16 | const _help = ''' 17 | 18 | Custom lint runner commands: 19 | r: Force re-lint 20 | q: Quit 21 | 22 | '''; 23 | 24 | /// Runs plugins with custom_lint.dart on the given directory 25 | /// 26 | /// In watch mode: 27 | /// * This will run until the user types q to quit 28 | /// * The plugin will hot-reload when the user changes it's code, and will cause a re-lint 29 | /// * The exit code is the one from the last lint before quitting 30 | /// * The user can force a reload by typing r 31 | /// 32 | /// Otherwise: 33 | /// * There is no hot-reload or watching so linting only happens once 34 | /// * The process exits with the most recent result of the linter 35 | /// 36 | /// Watch mode cannot be enabled if in release mode. 37 | Future customLint({ 38 | bool watchMode = true, 39 | required Directory workingDirectory, 40 | bool fatalInfos = true, 41 | bool fatalWarnings = true, 42 | OutputFormatEnum format = OutputFormatEnum.plain, 43 | bool fix = false, 44 | }) async { 45 | // Reset the code 46 | exitCode = 0; 47 | 48 | final channel = ServerIsolateChannel(); 49 | try { 50 | await _runServer( 51 | channel, 52 | watchMode: watchMode, 53 | workingDirectory: workingDirectory, 54 | fatalInfos: fatalInfos, 55 | fatalWarnings: fatalWarnings, 56 | format: format, 57 | fix: fix, 58 | ); 59 | } catch (_) { 60 | exitCode = 1; 61 | } finally { 62 | await channel.close(); 63 | } 64 | } 65 | 66 | Future _runServer( 67 | ServerIsolateChannel channel, { 68 | required bool watchMode, 69 | required Directory workingDirectory, 70 | required bool fatalInfos, 71 | required bool fatalWarnings, 72 | required OutputFormatEnum format, 73 | required bool fix, 74 | }) async { 75 | final customLintServer = await CustomLintServer.start( 76 | sendPort: channel.receivePort.sendPort, 77 | watchMode: watchMode, 78 | fix: fix, 79 | workingDirectory: workingDirectory, 80 | // In the CLI, only show user defined lints. Errors & logs will be 81 | // rendered separately 82 | includeBuiltInLints: false, 83 | delegate: CommandCustomLintDelegate(), 84 | ); 85 | 86 | await CustomLintServer.runZoned(() => customLintServer, () async { 87 | CustomLintRunner? runner; 88 | 89 | try { 90 | final workspace = await CustomLintWorkspace.fromPaths( 91 | [workingDirectory.path], 92 | workingDirectory: workingDirectory, 93 | ); 94 | runner = CustomLintRunner(customLintServer, workspace, channel); 95 | 96 | await runner.initialize; 97 | 98 | final log = CliLogger(); 99 | final progress = 100 | format == OutputFormatEnum.json ? null : log.progress('Analyzing'); 101 | 102 | await _runPlugins( 103 | runner, 104 | log: log, 105 | progress: progress, 106 | reload: false, 107 | workingDirectory: workingDirectory, 108 | fatalInfos: fatalInfos, 109 | fatalWarnings: fatalWarnings, 110 | format: format, 111 | ); 112 | 113 | if (watchMode) { 114 | await _startWatchMode( 115 | runner, 116 | log: log, 117 | workingDirectory: workingDirectory, 118 | fatalInfos: fatalInfos, 119 | fatalWarnings: fatalWarnings, 120 | format: format, 121 | ); 122 | } 123 | } finally { 124 | await runner?.close(); 125 | } 126 | }).whenComplete(() async { 127 | // Closing the server output of "runZoned" to ensure that "runZoned" completes 128 | // before the server is closed. 129 | // Failing to do so could cause exceptions within "runZoned" to be handled 130 | // after the server is closed, preventing the exception from being printed. 131 | await customLintServer.close(); 132 | }); 133 | } 134 | 135 | Future _runPlugins( 136 | CustomLintRunner runner, { 137 | required Logger log, 138 | required bool reload, 139 | required Directory workingDirectory, 140 | required bool fatalInfos, 141 | required bool fatalWarnings, 142 | required OutputFormatEnum format, 143 | Progress? progress, 144 | }) async { 145 | final lints = await runner.getLints(reload: reload); 146 | 147 | renderLints( 148 | lints, 149 | log: log, 150 | progress: progress, 151 | workingDirectory: workingDirectory, 152 | fatalInfos: fatalInfos, 153 | fatalWarnings: fatalWarnings, 154 | format: format, 155 | ); 156 | } 157 | 158 | Future _startWatchMode( 159 | CustomLintRunner runner, { 160 | required Logger log, 161 | required Directory workingDirectory, 162 | required bool fatalInfos, 163 | required bool fatalWarnings, 164 | required OutputFormatEnum format, 165 | }) async { 166 | if (stdin.hasTerminal) { 167 | stdin 168 | // Let's not pollute the output with whatever the user types 169 | ..echoMode = false 170 | // Let's not force user to have to press "enter" to input a command 171 | ..lineMode = false; 172 | } 173 | 174 | log.stdout(_help); 175 | 176 | // Handle user inputs, forcing the command to continue until the user asks to "quit" 177 | await for (final input in stdin.transform(utf8.decoder)) { 178 | switch (input) { 179 | case 'r': 180 | // Rerunning lints 181 | final progress = log.progress('Manual re-lint'); 182 | await _runPlugins( 183 | runner, 184 | log: log, 185 | progress: progress, 186 | reload: true, 187 | workingDirectory: workingDirectory, 188 | fatalInfos: fatalInfos, 189 | fatalWarnings: fatalWarnings, 190 | format: format, 191 | ); 192 | case 'q': 193 | // Let's quit the command line 194 | return; 195 | default: 196 | // Unknown command. Nothing to do 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/analyzer_plugin_starter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:ci/ci.dart' as ci; 5 | 6 | import 'plugin_delegate.dart'; 7 | import 'v2/custom_lint_analyzer_plugin.dart'; 8 | 9 | /// Connects custom_lint to the analyzer server using the analyzer_plugin protocol 10 | Future start(Iterable _, SendPort sendPort) async { 11 | final isInCI = ci.isCI; 12 | 13 | await CustomLintServer.start( 14 | sendPort: sendPort, 15 | includeBuiltInLints: true, 16 | // The IDE client should write to files, as what's visible in the editor 17 | // may not be the same as what's on disk. 18 | fix: false, 19 | // "start" may be run by `dart analyze`, in which case we don't want to 20 | // enable watch mode. There's no way to detect this, but this only matters 21 | // in the CI. So we disable watch mode if we detect that we're in CI. 22 | // TODO enable hot-restart only if running plugin from source (excluding pub cache) 23 | watchMode: isInCI ? false : null, 24 | delegate: AnalyzerPluginCustomLintDelegate(), 25 | workingDirectory: Directory.current, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/analyzer_utils/analyzer_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/file_system/file_system.dart'; 2 | // ignore: implementation_imports, not exported 3 | import 'package:analyzer/src/dart/analysis/byte_store.dart'; 4 | // ignore: implementation_imports, not exported 5 | import 'package:analyzer/src/dart/analysis/file_byte_store.dart'; 6 | 7 | /// Adds [createByteStore]. 8 | extension CreateByteStore on ResourceProvider { 9 | /// Obtains the location of a [ByteStore]. 10 | String getByteStorePath(String pluginID) { 11 | final stateLocation = getStateLocation(pluginID); 12 | 13 | if (stateLocation == null) { 14 | throw StateError('Failed to obtain the byte store path'); 15 | } 16 | 17 | return stateLocation.path; 18 | } 19 | 20 | /// If the state location can be accessed, return the file byte store, 21 | /// otherwise return the memory byte store. 22 | ByteStore createByteStore(String pluginID) { 23 | const M = 1024 * 1024; 24 | 25 | return MemoryCachingByteStore( 26 | FileByteStore( 27 | getByteStorePath(pluginID), 28 | tempNameSuffix: DateTime.now().millisecondsSinceEpoch.toString(), 29 | ), 30 | 64 * M, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/async_operation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// An extension on [Stream] that adds a [safeFirst] method. 4 | extension StreamFirst on Stream { 5 | /// A fork of [first] meant to be used instead of [first], to possibly override 6 | /// it during debugging to provide more information. 7 | Future get safeFirst => first; 8 | } 9 | 10 | /// A class for awaiting multiple async operations at once. 11 | /// 12 | /// See [wait]. 13 | class PendingOperation { 14 | final _pendingOperations = >[]; 15 | 16 | /// Register an async operation to be awaited. 17 | Future run(Future Function() cb) async { 18 | final future = cb(); 19 | 20 | _pendingOperations.add(future); 21 | try { 22 | return await future; 23 | } finally { 24 | _pendingOperations.remove(future); 25 | } 26 | } 27 | 28 | /// Waits for all operations registered in [run]. 29 | /// 30 | /// If during the wait new async operations are registered, they will be 31 | /// awaited too. 32 | Future wait() async { 33 | /// Wait for all pending operations to complete and check that no new 34 | /// operations are queued for a few consecutive frames. 35 | while (_pendingOperations.isNotEmpty) { 36 | await Future.wait(_pendingOperations.toList()) 37 | // Catches errors to make sure that errors inside operations don't 38 | // abort the "wait" early 39 | .then((value) => null, onError: (_) {}); 40 | } 41 | } 42 | } 43 | 44 | /// Workaround to a limitation in [runZonedGuarded] that states the following: 45 | /// 46 | /// > The zone will always be an error-zone ([Zone.errorZone]), so returning a 47 | /// > future created inside the zone, and waiting for it outside of the zone, 48 | /// > will risk the future not being seen to complete. 49 | /// 50 | /// This function solves the issue by creating a [Completer] outside of 51 | /// [runZonedGuarded] and completing it inside the zone. This way, the future 52 | /// is created outside of the zone and can safely be awaited. 53 | Future asyncRunZonedGuarded( 54 | FutureOr Function() body, 55 | void Function(Object error, StackTrace stack) onError, { 56 | Map? zoneValues, 57 | ZoneSpecification? zoneSpecification, 58 | }) async { 59 | final completer = Completer(); 60 | 61 | unawaited( 62 | runZonedGuarded( 63 | () => Future(body).then( 64 | completer.complete, 65 | // ignore: avoid_types_on_closure_parameters, false positive 66 | onError: (Object error, StackTrace stack) { 67 | // Make sure the initial error is also reported. 68 | onError(error, stack); 69 | 70 | completer.completeError(error, stack); 71 | }, 72 | ), 73 | onError, 74 | zoneSpecification: zoneSpecification, 75 | ), 76 | ); 77 | 78 | return completer.future; 79 | } 80 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/channels.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:isolate'; 5 | import 'dart:typed_data'; 6 | 7 | import 'package:analyzer_plugin/protocol/protocol.dart'; 8 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 9 | // ignore: implementation_imports, not exported 10 | import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' 11 | show ResponseResult; 12 | 13 | /// An interface for interacting with analyzer_plugin 14 | abstract class AnalyzerPluginClientChannel { 15 | /// The list of messages sent by analyzer_plugin. 16 | Stream get messages; 17 | 18 | /// Sends a JSON object to the analyzer_plugin server 19 | void sendJson(Map json); 20 | 21 | /// Sends a [Response] to the analyzer_plugin server. 22 | void sendResponse({ 23 | ResponseResult? data, 24 | RequestError? error, 25 | required String requestID, 26 | required int requestTime, 27 | }) { 28 | sendJson( 29 | Response( 30 | requestID, 31 | requestTime, 32 | result: data?.toJson(), 33 | error: error, 34 | ).toJson(), 35 | ); 36 | } 37 | 38 | /// Releases the resources 39 | Future close(); 40 | } 41 | 42 | /// The number of bytes used to store the length of a message 43 | const _lengthBytes = 4; 44 | 45 | /// An interface for discussing with analyzer_plugin using a [SendPort] 46 | class JsonSendPortChannel extends AnalyzerPluginClientChannel { 47 | /// An interface for discussing with analyzer_plugin using a [SendPortƒ] 48 | JsonSendPortChannel(this._sendPort) : _receivePort = ReceivePort() { 49 | _sendPort.send(_receivePort.sendPort); 50 | } 51 | 52 | final SendPort _sendPort; 53 | final ReceivePort _receivePort; 54 | 55 | @override 56 | late final Stream messages = _receivePort.asBroadcastStream(); 57 | 58 | @override 59 | void sendJson(Map json) { 60 | _sendPort.send(json); 61 | } 62 | 63 | @override 64 | Future close() async { 65 | _receivePort.close(); 66 | } 67 | } 68 | 69 | /// An interface for discussing with analyzer_plugin using web sockets 70 | class JsonSocketChannel extends AnalyzerPluginClientChannel { 71 | /// An interface for discussing with analyzer_plugin using web sockets 72 | JsonSocketChannel(this._socket) { 73 | // Pipe the socket messages in a broadcast stream 74 | _subscription = Stream.fromFuture(_socket).asyncExpand((e) => e).listen( 75 | _controller.add, 76 | onError: _controller.addError, 77 | onDone: _controller.close, 78 | ); 79 | } 80 | 81 | final Future _socket; 82 | 83 | final _controller = StreamController.broadcast(); 84 | late StreamSubscription _subscription; 85 | 86 | /// Send a message while having the first 4 bytes of the message be the length of the message. 87 | void _sendWithLength(Socket socket, List data) { 88 | final length = data.length; 89 | final buffer = Uint8List(_lengthBytes + length); 90 | final byteData = ByteData.view(buffer.buffer); 91 | 92 | byteData.setUint32(0, length); 93 | buffer.setRange(_lengthBytes, _lengthBytes + length, data); 94 | socket.add(buffer); 95 | } 96 | 97 | /// The [sendJson] method have messages start with the message length, 98 | /// because a chunk of data can contain multiple separate messages. 99 | /// 100 | /// By sending the length with every message, the receiver can know 101 | /// where a message ends and another begins. 102 | Iterable> _receiveWithLength(Uint8List input) sync* { 103 | final chunk = ByteData.view(input.buffer); 104 | 105 | var startOffset = 0; 106 | var bytesCountNeeded = _lengthBytes; 107 | var isReadingMessageLength = true; 108 | 109 | while (startOffset + bytesCountNeeded <= input.length) { 110 | if (isReadingMessageLength) { 111 | // Reading the length of the next message. 112 | bytesCountNeeded = chunk.getUint32(startOffset); 113 | 114 | // We have the message length, now reading the message. 115 | startOffset += _lengthBytes; 116 | isReadingMessageLength = false; 117 | } else { 118 | // We have the message length, now reading the message. 119 | final message = input.sublist( 120 | startOffset, 121 | startOffset + bytesCountNeeded, 122 | ); 123 | yield message; 124 | 125 | // Reset to reading the length of the next message. 126 | startOffset += bytesCountNeeded; 127 | bytesCountNeeded = _lengthBytes; 128 | isReadingMessageLength = true; 129 | } 130 | } 131 | } 132 | 133 | @override 134 | late final Stream messages = _controller.stream 135 | .expand(_receiveWithLength) 136 | .map(utf8.decode) 137 | .map(jsonDecode); 138 | 139 | @override 140 | Future sendJson(Map json) async { 141 | final socket = await _socket; 142 | 143 | _sendWithLength( 144 | socket, 145 | utf8.encode(jsonEncode(json)), 146 | ); 147 | } 148 | 149 | @override 150 | Future close() async { 151 | await Future.wait([ 152 | _subscription.cancel(), 153 | _controller.close(), 154 | ]); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/cli_logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' as io; 2 | 3 | import 'package:cli_util/cli_logging.dart'; 4 | 5 | /// Temporary copy of [StandardLogger] from `cli_util` package with a fix 6 | /// for https://github.com/dart-lang/cli_util/pull/87 7 | /// which replaces print with stdout.writeln. 8 | class CliLogger implements Logger { 9 | /// Creates a cli logger with ANSI support 10 | /// that writes messages and progress [io.stdout]. 11 | CliLogger({Ansi? ansi}) : ansi = ansi ?? Ansi(io.stdout.supportsAnsiEscapes); 12 | 13 | @override 14 | Ansi ansi; 15 | 16 | @override 17 | bool get isVerbose => false; 18 | 19 | Progress? _currentProgress; 20 | 21 | @override 22 | void stderr(String message) { 23 | _cancelProgress(); 24 | 25 | io.stderr.writeln(message); 26 | } 27 | 28 | @override 29 | void stdout(String message) { 30 | _cancelProgress(); 31 | 32 | io.stdout.writeln(message); 33 | } 34 | 35 | @override 36 | void trace(String message) {} 37 | 38 | @override 39 | void write(String message) { 40 | _cancelProgress(); 41 | 42 | io.stdout.write(message); 43 | } 44 | 45 | @override 46 | void writeCharCode(int charCode) { 47 | _cancelProgress(); 48 | 49 | io.stdout.writeCharCode(charCode); 50 | } 51 | 52 | void _cancelProgress() { 53 | final progress = _currentProgress; 54 | if (progress != null) { 55 | _currentProgress = null; 56 | progress.cancel(); 57 | } 58 | } 59 | 60 | @override 61 | Progress progress(String message) { 62 | _cancelProgress(); 63 | 64 | final progress = _LineOnFinishProgress( 65 | ansi.useAnsi 66 | ? AnsiProgress(ansi, message) 67 | : SimpleProgress(this, message), 68 | log: this, 69 | ); 70 | _currentProgress = progress; 71 | return progress; 72 | } 73 | 74 | @override 75 | @Deprecated('This method will be removed in the future') 76 | void flush() {} 77 | } 78 | 79 | class _LineOnFinishProgress implements Progress { 80 | const _LineOnFinishProgress(this.impl, {required this.log}); 81 | 82 | final CliLogger log; 83 | final Progress impl; 84 | 85 | @override 86 | Duration get elapsed => impl.elapsed; 87 | 88 | @override 89 | String get message => impl.message; 90 | 91 | @override 92 | void cancel() { 93 | impl.cancel(); 94 | } 95 | 96 | @override 97 | void finish({String? message, bool showTiming = false}) { 98 | impl.finish(message: message, showTiming: showTiming); 99 | 100 | // Separate progress from results 101 | log.stdout(''); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/client_isolate_channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:analyzer_plugin/channel/channel.dart'; 5 | import 'package:analyzer_plugin/protocol/protocol.dart'; 6 | 7 | /// A channel used to communicate with the analyzer server using the 8 | /// analyzer_plugin protocol 9 | /// 10 | /// Imported from package:analyzer_plugin 11 | class ClientIsolateChannel implements PluginCommunicationChannel { 12 | /// Initialize a newly created channel to communicate with the server. 13 | ClientIsolateChannel(this._sendPort) { 14 | _receivePort = ReceivePort(); 15 | _sendPort.send(_receivePort.sendPort); 16 | } 17 | 18 | /// The port used to send notifications and responses to the server. 19 | final SendPort _sendPort; 20 | 21 | /// The port used to receive requests from the server. 22 | late final ReceivePort _receivePort; 23 | 24 | /// The subscription that needs to be cancelled when the channel is closed. 25 | StreamSubscription? _subscription; 26 | 27 | @override 28 | void close() { 29 | unawaited(_subscription?.cancel()); 30 | _subscription = null; 31 | _receivePort.close(); 32 | } 33 | 34 | @override 35 | void listen( 36 | void Function(Request request) onRequest, { 37 | Function? onError, 38 | void Function()? onDone, 39 | }) { 40 | void onData(Object? data) { 41 | final requestMap = data! as Map; 42 | final request = Request.fromJson(requestMap); 43 | onRequest(request); 44 | } 45 | 46 | if (_subscription != null) { 47 | throw StateError('Only one listener is allowed per channel'); 48 | } 49 | _subscription = _receivePort.listen( 50 | onData, 51 | onError: onError, 52 | onDone: onDone, 53 | cancelOnError: false, 54 | ); 55 | } 56 | 57 | @override 58 | void sendNotification(Notification notification) { 59 | final json = notification.toJson(); 60 | _sendPort.send(json); 61 | } 62 | 63 | @override 64 | void sendResponse(Response response) { 65 | final json = response.toJson(); 66 | _sendPort.send(json); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/output/default_output_format.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer_plugin/protocol/protocol_common.dart'; 2 | import 'package:cli_util/cli_logging.dart'; 3 | 4 | import 'output_format.dart'; 5 | import 'render_lints.dart'; 6 | 7 | /// The default output format. 8 | class DefaultOutputFormat implements OutputFormat { 9 | @override 10 | void render({ 11 | required Iterable errors, 12 | required Logger log, 13 | }) { 14 | if (errors.isEmpty) { 15 | log.stdout('No issues found!'); 16 | return; 17 | } 18 | 19 | for (final error in errors) { 20 | log.stdout( 21 | ' ${error.location.relativePath}:${error.location.startLine}:${error.location.startColumn}' 22 | ' • ${error.message} • ${error.code} • ${error.severity.name}', 23 | ); 24 | } 25 | 26 | // Display a summary separated from the lints 27 | log.stdout(''); 28 | final errorCount = errors.length; 29 | log.stdout('$errorCount issue${errorCount > 1 ? 's' : ''} found.'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/output/json_output_format.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:analyzer_plugin/protocol/protocol_common.dart'; 4 | import 'package:cli_util/cli_logging.dart'; 5 | 6 | import 'output_format.dart'; 7 | 8 | /// The JSON output format. 9 | /// 10 | /// Code is an adaption of the original Dart SDK JSON format. 11 | /// See: https://github.com/dart-lang/sdk/blob/main/pkg/dartdev/lib/src/commands/analyze.dart 12 | class JsonOutputFormat implements OutputFormat { 13 | @override 14 | void render({ 15 | required Iterable errors, 16 | required Logger log, 17 | }) { 18 | final diagnostics = >[]; 19 | for (final error in errors) { 20 | final contextMessages = >[]; 21 | if (error.contextMessages != null) { 22 | for (final contextMessage in error.contextMessages!) { 23 | final startOffset = contextMessage.location.offset; 24 | contextMessages.add({ 25 | 'location': _location( 26 | file: contextMessage.location.file, 27 | range: _range( 28 | start: _position( 29 | offset: startOffset, 30 | line: contextMessage.location.startLine, 31 | column: contextMessage.location.startColumn, 32 | ), 33 | end: _position( 34 | offset: startOffset + contextMessage.location.length, 35 | line: contextMessage.location.endLine, 36 | column: contextMessage.location.endColumn, 37 | ), 38 | ), 39 | ), 40 | 'message': contextMessage.message, 41 | }); 42 | } 43 | } 44 | final startOffset = error.location.offset; 45 | diagnostics.add({ 46 | 'code': error.code, 47 | 'severity': error.severity, 48 | 'type': error.type, 49 | 'location': _location( 50 | file: error.location.file, 51 | range: _range( 52 | start: _position( 53 | offset: startOffset, 54 | line: error.location.startLine, 55 | column: error.location.startColumn, 56 | ), 57 | end: _position( 58 | offset: startOffset + error.location.length, 59 | line: error.location.endLine, 60 | column: error.location.endColumn, 61 | ), 62 | ), 63 | ), 64 | 'problemMessage': error.message, 65 | if (error.correction != null) 'correctionMessage': error.correction, 66 | if (contextMessages.isNotEmpty) 'contextMessages': contextMessages, 67 | if (error.url != null) 'documentation': error.url, 68 | }); 69 | } 70 | log.stdout( 71 | json.encode({ 72 | 'version': 1, 73 | 'diagnostics': diagnostics, 74 | }), 75 | ); 76 | } 77 | 78 | Map _location({ 79 | required String file, 80 | required Map range, 81 | }) { 82 | return { 83 | 'file': file, 84 | 'range': range, 85 | }; 86 | } 87 | 88 | Map _position({ 89 | int? offset, 90 | int? line, 91 | int? column, 92 | }) { 93 | return { 94 | 'offset': offset, 95 | 'line': line, 96 | 'column': column, 97 | }; 98 | } 99 | 100 | Map _range({ 101 | required Map start, 102 | required Map end, 103 | }) { 104 | return { 105 | 'start': start, 106 | 'end': end, 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/output/output_format.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer_plugin/protocol/protocol_common.dart'; 2 | import 'package:cli_util/cli_logging.dart'; 3 | 4 | /// An enum for the output format. 5 | enum OutputFormatEnum { 6 | /// The default output format. 7 | plain._('default'), 8 | 9 | /// Dart SDK like JSON output format. 10 | json._('json'); 11 | 12 | const OutputFormatEnum._(this.name); 13 | 14 | /// The name of the format. 15 | final String name; 16 | 17 | /// Returns the [OutputFormatEnum] for the given [name]. 18 | static OutputFormatEnum fromName(String name) { 19 | for (final format in OutputFormatEnum.values) { 20 | if (format.name == name) { 21 | return format; 22 | } 23 | } 24 | return plain; 25 | } 26 | } 27 | 28 | /// An abstract class for outputting lints 29 | abstract class OutputFormat { 30 | /// Renders lints according to the format and flags. 31 | void render({ 32 | required Iterable errors, 33 | required Logger log, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/output/render_lints.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:analyzer_plugin/protocol/protocol_common.dart'; 3 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 4 | import 'package:cli_util/cli_logging.dart'; 5 | import 'package:collection/collection.dart'; 6 | import 'package:path/path.dart' as p; 7 | 8 | import 'default_output_format.dart'; 9 | import 'json_output_format.dart'; 10 | import 'output_format.dart'; 11 | 12 | /// Renders lints according to the given format and flags. 13 | void renderLints( 14 | List lints, { 15 | required Logger log, 16 | required Directory workingDirectory, 17 | required bool fatalInfos, 18 | required bool fatalWarnings, 19 | required OutputFormatEnum format, 20 | Progress? progress, 21 | }) { 22 | final OutputFormat outputFormat; 23 | switch (format) { 24 | case OutputFormatEnum.json: 25 | outputFormat = JsonOutputFormat(); 26 | case OutputFormatEnum.plain: 27 | // ignore: unreachable_switch_default, disable for now as won't work on stable 28 | default: 29 | outputFormat = DefaultOutputFormat(); 30 | } 31 | 32 | var errors = lints.expand((lint) => lint.errors); 33 | 34 | var fatal = false; 35 | for (final error in errors) { 36 | error.location.relativePath = p.relative( 37 | error.location.file, 38 | from: workingDirectory.absolute.path, 39 | ); 40 | fatal = fatal || 41 | error.severity == AnalysisErrorSeverity.ERROR || 42 | (fatalWarnings && error.severity == AnalysisErrorSeverity.WARNING) || 43 | (fatalInfos && error.severity == AnalysisErrorSeverity.INFO); 44 | } 45 | 46 | // Sort errors by severity, file, line, column, code, message 47 | // if the output format requires it 48 | errors = errors.sorted((a, b) { 49 | final severityCompare = -AnalysisErrorSeverity.values 50 | .indexOf(a.severity) 51 | .compareTo(AnalysisErrorSeverity.values.indexOf(b.severity)); 52 | if (severityCompare != 0) return severityCompare; 53 | 54 | final fileCompare = 55 | a.location.relativePath.compareTo(b.location.relativePath); 56 | if (fileCompare != 0) return fileCompare; 57 | 58 | final lineCompare = a.location.startLine.compareTo(b.location.startLine); 59 | if (lineCompare != 0) return lineCompare; 60 | 61 | final columnCompare = 62 | a.location.startColumn.compareTo(b.location.startColumn); 63 | if (columnCompare != 0) return columnCompare; 64 | 65 | final codeCompare = a.code.compareTo(b.code); 66 | if (codeCompare != 0) return codeCompare; 67 | 68 | return a.message.compareTo(b.message); 69 | }); 70 | 71 | // Finish progress and display duration (only when ANSI is supported) 72 | progress?.finish(showTiming: true); 73 | 74 | outputFormat.render( 75 | errors: errors, 76 | log: log, 77 | ); 78 | 79 | if (fatal) { 80 | exitCode = 1; 81 | return; 82 | } 83 | } 84 | 85 | final _locationRelativePath = Expando('locationRelativePath'); 86 | 87 | /// A helper extension to set/get 88 | /// the working directory relative path of a [Location]. 89 | extension LocationRelativePath on Location { 90 | /// The working directory relative path of this [Location]. 91 | String get relativePath => _locationRelativePath[this]! as String; 92 | 93 | set relativePath(String path) => _locationRelativePath[this] = path; 94 | } 95 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/runner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 4 | import 'package:cli_util/cli_util.dart'; 5 | 6 | import 'server_isolate_channel.dart'; 7 | import 'v2/custom_lint_analyzer_plugin.dart'; 8 | import 'workspace.dart'; 9 | 10 | const _analyzerPluginProtocolVersion = '1.0.0-alpha.0'; 11 | 12 | /// A runner for programmatically interacting with a plugin. 13 | class CustomLintRunner { 14 | /// A runner for programmatically interacting with a plugin. 15 | CustomLintRunner(this._server, this.workspace, this.channel); 16 | 17 | /// The custom_lint project that is being run. 18 | final CustomLintWorkspace workspace; 19 | 20 | /// The connection between the server and the plugin. 21 | final ServerIsolateChannel channel; 22 | final CustomLintServer _server; 23 | final _accumulatedLints = {}; 24 | StreamSubscription? _lintSubscription; 25 | 26 | var _closed = false; 27 | 28 | /// Starts the plugin and sends the necessary requests for initializing it. 29 | late final initialize = Future(() async { 30 | _lintSubscription = channel.lints.listen((event) { 31 | _accumulatedLints[event.file] = event; 32 | }); 33 | 34 | await channel.sendRequest( 35 | PluginVersionCheckParams( 36 | '', 37 | sdkPath, 38 | _analyzerPluginProtocolVersion, 39 | ), 40 | ); 41 | await channel.sendRequest( 42 | AnalysisSetContextRootsParams(workspace.contextRoots), 43 | ); 44 | }); 45 | 46 | /// Obtains the list of lints for the current workspace. 47 | Future> getLints({required bool reload}) async { 48 | if (reload) _accumulatedLints.clear(); 49 | 50 | await _server.awaitAnalysisDone(reload: reload); 51 | 52 | return _accumulatedLints.values.toList() 53 | ..sort((a, b) => a.file.compareTo(b.file)); 54 | } 55 | 56 | /// Obtains the list of fixes for a given file/offset combo 57 | Future getFixes( 58 | String path, 59 | int offset, 60 | ) async { 61 | final result = await channel.sendRequest( 62 | EditGetFixesParams(path, offset), 63 | ); 64 | return EditGetFixesResult.fromResponse(result); 65 | } 66 | 67 | /// Stop the command runner, sending a [PluginShutdownParams] request in the process. 68 | Future close() async { 69 | if (_closed) return; 70 | _closed = true; 71 | 72 | try { 73 | await channel.sendRequest(PluginShutdownParams()); 74 | } finally { 75 | await _lintSubscription?.cancel(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/server_isolate_channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:analyzer_plugin/protocol/protocol.dart'; 5 | import 'package:analyzer_plugin/protocol/protocol_constants.dart'; 6 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 7 | // ignore: implementation_imports, not exported 8 | import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' 9 | show RequestParams; 10 | import 'package:async/async.dart'; 11 | import 'package:uuid/uuid.dart'; 12 | 13 | import 'async_operation.dart'; 14 | 15 | const _uuid = Uuid(); 16 | 17 | /// A base class for the protocol responsible with interacting with using the 18 | /// analyzer_plugin API 19 | mixin ChannelBase { 20 | /// The stream containing any message from the client 21 | Stream get inputStream; 22 | 23 | /// The [Notification]s emitted by the plugin 24 | late final Stream notifications = inputStream 25 | .where((e) => e is Map) 26 | .map((e) => e! as Map) 27 | .where((e) => e.containsKey(Notification.EVENT)) 28 | .map(Notification.fromJson); 29 | 30 | /// The [Response]s emitted by the plugin 31 | late final Stream responses = inputStream 32 | .where((event) => event is Map) 33 | .map((event) => event! as Map) 34 | .where((e) => e.containsKey(Response.ID)) 35 | .map(Response.fromJson); 36 | 37 | /// Error [Notification]s. 38 | late final Stream pluginErrors = 39 | StreamGroup.mergeBroadcast([ 40 | // Manual error notifications from the plugin 41 | notifications 42 | .where((e) => e.event == PLUGIN_NOTIFICATION_ERROR) 43 | .map(PluginErrorParams.fromNotification), 44 | 45 | // When the receivePort is passed to Isolate.onError, error events are 46 | // received as ["error", "stackTrace"] 47 | inputStream 48 | .where((event) => event is List) 49 | .cast>() 50 | .map((event) { 51 | final error = event.first.toString(); 52 | final stackTrace = event.last.toString(); 53 | return PluginErrorParams(false, error, stackTrace); 54 | }), 55 | ]); 56 | 57 | /// Errors for [Request]s that failed. 58 | late final Stream responseErrors = 59 | responses.where((e) => e.error != null).map((e) => e.error!); 60 | 61 | /// Sends a json object to the plugin, without awaiting for an answer 62 | Future sendJson(Map json); 63 | 64 | /// Send a request and obtains the associated response 65 | Future sendRequest( 66 | RequestParams requestParams, 67 | ) async { 68 | final id = _uuid.v4(); 69 | final request = requestParams.toRequest(id); 70 | final responseFuture = responses.firstWhere( 71 | (message) { 72 | return message.id == id; 73 | }, 74 | orElse: () => throw StateError( 75 | 'No response for request ${request.method} $id', 76 | ), 77 | ); 78 | await sendJson(request.toJson()); 79 | final response = await responseFuture; 80 | 81 | if (response.error != null) { 82 | throw _PrettyRequestFailure(response.error!); 83 | } 84 | 85 | return response; 86 | } 87 | 88 | /// Send a request and obtains the associated response 89 | Future sendRequestParams( 90 | RequestParams requestParams, 91 | ) async { 92 | final id = _uuid.v4(); 93 | 94 | final request = requestParams.toRequest(id); 95 | final responseFuture = responses.firstWhere( 96 | (message) => message.id == id, 97 | orElse: () => throw StateError( 98 | 'No response for request ${request.method} $id', 99 | ), 100 | ); 101 | await sendJson(request.toJson()); 102 | final response = await responseFuture; 103 | 104 | if (response.error != null) { 105 | throw _PrettyRequestFailure(response.error!); 106 | } 107 | 108 | return response; 109 | } 110 | } 111 | 112 | class _PrettyRequestFailure extends RequestFailure { 113 | _PrettyRequestFailure(super.error); 114 | 115 | @override 116 | String toString() { 117 | return '_PrettyRequestFailure: $error'; 118 | } 119 | } 120 | 121 | /// Mixin for Isolate-based channels 122 | abstract class IsolateChannelBase with ChannelBase { 123 | /// Mixin for Isolate-based channels 124 | IsolateChannelBase(this.receivePort) { 125 | _sendPort = inputStream 126 | .where((event) => event is SendPort) 127 | .cast() 128 | .safeFirst; 129 | } 130 | 131 | /// The [ReceivePort] responsible for listening to requests. 132 | final ReceivePort receivePort; 133 | 134 | @override 135 | late final Stream inputStream = receivePort.asBroadcastStream(); 136 | 137 | /// The [SendPort] responsible for sending events to the isolate. 138 | late final Future _sendPort; 139 | 140 | @override 141 | Future sendJson(Map json) { 142 | return _sendPort.then((value) => value.send(json)); 143 | } 144 | } 145 | 146 | /// An interface for interacting with the plugin server. 147 | class ServerIsolateChannel extends IsolateChannelBase { 148 | /// An interface for interacting with the plugin server. 149 | ServerIsolateChannel() : super(ReceivePort()); 150 | 151 | /// Lints emitted by the plugin 152 | late final Stream lints = notifications 153 | .where((e) => e.event == ANALYSIS_NOTIFICATION_ERRORS) 154 | .map(AnalysisErrorsParams.fromNotification); 155 | 156 | /// Releases the associated resources. 157 | Future close() async { 158 | receivePort.close(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /packages/custom_lint/lib/src/v2/protocol.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer_plugin/protocol/protocol.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'protocol.g.dart'; 5 | part 'protocol.freezed.dart'; 6 | 7 | /// A base class shared between all custom_lint requests 8 | @freezed 9 | sealed class CustomLintRequest with _$CustomLintRequest { 10 | /// A request using the analyzer_plugin protocol 11 | factory CustomLintRequest.analyzerPluginRequest( 12 | Request request, { 13 | required String id, 14 | }) = CustomLintRequestAnalyzerPluginRequest; 15 | 16 | /// Requests to wait for the client to complete its analysis 17 | factory CustomLintRequest.awaitAnalysisDone({ 18 | required String id, 19 | required bool reload, 20 | }) = CustomLintRequestAwaitAnalysisDone; 21 | 22 | /// Sends a meaningless message to the client, waiting for a response. 23 | factory CustomLintRequest.ping({required String id}) = CustomLintRequestPing; 24 | 25 | /// Decode a custom_lint request from JSON 26 | factory CustomLintRequest.fromJson(Map json) => 27 | _$CustomLintRequestFromJson(json); 28 | 29 | /// The unique request ID 30 | @override 31 | String get id; 32 | } 33 | 34 | /// The base class for all responses to a custom_lint request. 35 | @freezed 36 | sealed class CustomLintResponse with _$CustomLintResponse { 37 | /// The response for an analyzer_plugin request 38 | factory CustomLintResponse.analyzerPluginResponse( 39 | Response response, { 40 | required String id, 41 | }) = CustomLintResponseAnalyzerPluginResponse; 42 | 43 | /// The message sent when the client has completed its analysis 44 | factory CustomLintResponse.awaitAnalysisDone({required String id}) = 45 | CustomLintResponseAwaitAnalysisDone; 46 | 47 | /// The reply to a ping request 48 | factory CustomLintResponse.pong({required String id}) = 49 | CustomLintResponsePong; 50 | 51 | /// A request failed 52 | factory CustomLintResponse.error({ 53 | required String id, 54 | required String message, 55 | required String stackTrace, 56 | }) = CustomLintResponseError; 57 | 58 | /// Decode a response from JSON 59 | factory CustomLintResponse.fromJson(Map json) => 60 | _$CustomLintResponseFromJson(json); 61 | 62 | @override 63 | String get id; 64 | } 65 | 66 | /// A base class between all messages from the client, be it request responses, 67 | /// or spontaneous events. 68 | @Freezed(copyWith: false) 69 | abstract class CustomLintMessage with _$CustomLintMessage { 70 | /// A spontaneous event, not associated with a request 71 | factory CustomLintMessage.event(CustomLintEvent event) = 72 | CustomLintMessageEvent; 73 | 74 | /// A response to a request 75 | factory CustomLintMessage.response(CustomLintResponse response) = 76 | CustomLintMessageResponse; 77 | 78 | /// Decode a message from JSONs 79 | factory CustomLintMessage.fromJson(Map json) => 80 | _$CustomLintMessageFromJson(json); 81 | } 82 | 83 | /// A class for decoding a [Notification]. 84 | class NotificationJsonConverter 85 | extends JsonConverter> { 86 | /// A class for decoding a [Notification]. 87 | const NotificationJsonConverter(); 88 | 89 | @override 90 | Notification fromJson(Map json) { 91 | return Notification( 92 | json[Notification.EVENT]! as String, 93 | Map.from(json[Notification.PARAMS]! as Map), 94 | ); 95 | } 96 | 97 | @override 98 | Map toJson(Notification object) { 99 | return object.toJson(); 100 | } 101 | } 102 | 103 | /// A base class for all custom_lint events 104 | @freezed 105 | sealed class CustomLintEvent with _$CustomLintEvent { 106 | /// The client sent a [Notification] using the analyzer_plugin protocol 107 | factory CustomLintEvent.analyzerPluginNotification( 108 | @NotificationJsonConverter() Notification notification, 109 | ) = CustomLintEventAnalyzerPluginNotification; 110 | // TODO add source change event? 111 | 112 | /// A spontaneous error, unrelated to a request 113 | factory CustomLintEvent.error( 114 | String message, 115 | String stackTrace, { 116 | required String? pluginName, 117 | }) = CustomLintEventError; 118 | 119 | /// A log output 120 | factory CustomLintEvent.print( 121 | String message, { 122 | required String? pluginName, 123 | }) = CustomLintEventPrint; 124 | 125 | /// Decode an event from JSON 126 | factory CustomLintEvent.fromJson(Map json) => 127 | _$CustomLintEventFromJson(json); 128 | } 129 | -------------------------------------------------------------------------------- /packages/custom_lint/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint 2 | version: 0.7.5 3 | 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. 4 | repository: https://github.com/invertase/dart_custom_lint 5 | issue_tracker: https://github.com/invertase/dart_custom_lint/issues 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | 10 | dependencies: 11 | analyzer: ^7.0.0 12 | analyzer_plugin: ^0.13.0 13 | args: ^2.3.1 14 | async: ^2.9.0 15 | ci: ^0.1.0 16 | cli_util: ^0.4.2 17 | collection: ^1.16.0 18 | custom_lint_core: 0.7.5 19 | freezed_annotation: ^3.0.0 20 | json_annotation: ^4.7.0 21 | meta: ^1.7.0 22 | package_config: ^2.0.2 23 | path: ^1.8.0 24 | pub_semver: ^2.1.1 25 | pubspec_parse: ^1.5.0 26 | rxdart: ^0.28.0 27 | uuid: ">=3.0.6 <5.0.0" 28 | yaml: ^3.1.1 29 | 30 | dev_dependencies: 31 | ansi_styles: ^0.3.2+1 32 | benchmark_harness: ^2.2.0 33 | build_runner: ^2.3.2 34 | file: ^7.0.0 35 | freezed: ^3.0.0 36 | glob: ^2.1.2 37 | json_serializable: ^6.5.4 38 | test: ^1.20.2 39 | test_process: ^2.1.0 40 | 41 | executables: 42 | custom_lint: custom_lint 43 | -------------------------------------------------------------------------------- /packages/custom_lint/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint_core,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint_core: 4 | path: ../custom_lint_core 5 | -------------------------------------------------------------------------------- /packages/custom_lint/test/equals_ignoring_ansi.dart: -------------------------------------------------------------------------------- 1 | import 'package:ansi_styles/extension.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | /// Returns a matcher which matches if the match argument is a string and 5 | /// is equal to [value] after removing ansi codes 6 | Matcher equalsIgnoringAnsi(String value) => _IsEqualIgnoringAnsi(null, value); 7 | 8 | Matcher matchIgnoringAnsi( 9 | Matcher Function(String value) matcher, 10 | String value, 11 | ) { 12 | return _IsEqualIgnoringAnsi(matcher(value), value); 13 | } 14 | 15 | class _IsEqualIgnoringAnsi extends Matcher { 16 | _IsEqualIgnoringAnsi(this.matcher, this._value); 17 | 18 | static final Object _mismatchedValueKey = Object(); 19 | static final Object _matcherKey = Object(); 20 | 21 | final Matcher? matcher; 22 | final String _value; 23 | 24 | @override 25 | bool matches(Object? object, Map matchState) { 26 | final description = (object! as String).strip.replaceAll(';49m', ''); 27 | final matcher = this.matcher; 28 | 29 | final isMatching = 30 | matcher?.matches(description, matchState) ?? _value == description; 31 | 32 | if (!isMatching) matchState[_mismatchedValueKey] = description; 33 | if (matcher != null) matchState[_matcherKey] = matcher; 34 | return isMatching; 35 | } 36 | 37 | @override 38 | Description describe(Description description) { 39 | return description.addDescriptionOf(_value).add(' ignoring ansi codes'); 40 | } 41 | 42 | @override 43 | Description describeMismatch( 44 | Object? item, 45 | Description mismatchDescription, 46 | Map matchState, 47 | bool verbose, 48 | ) { 49 | if (matchState.containsKey(_mismatchedValueKey)) { 50 | final actualValue = matchState[_mismatchedValueKey]! as String; 51 | final matcher = matchState[_matcherKey] as Matcher?; 52 | 53 | // Leading whitespace is added so that lines in the multiline 54 | // description returned by addDescriptionOf are all indented equally 55 | // which makes the output easier to read for this case. 56 | mismatchDescription.add('expected normalized value\n '); 57 | 58 | if (matcher != null) { 59 | mismatchDescription.add('\nto match\n ').addDescriptionOf(matcher); 60 | } else { 61 | mismatchDescription.addDescriptionOf(_value); 62 | } 63 | 64 | mismatchDescription.add('\nbut got\n '); 65 | mismatchDescription.addDescriptionOf(actualValue); 66 | } 67 | 68 | return mismatchDescription; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/custom_lint/test/error_report_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:custom_lint_core/custom_lint_core.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'cli_process_test.dart'; 8 | import 'create_project.dart'; 9 | import 'peer_project_meta.dart'; 10 | 11 | void main() { 12 | group('Reports errors', () { 13 | test('inside LintRule.startUp', () { 14 | final plugin = createPlugin( 15 | name: 'test_lint', 16 | main: createPluginSource([ 17 | TestLintRule( 18 | code: 'hello_world', 19 | message: 'Hello world', 20 | startUp: "throw StateError('hello');", 21 | ), 22 | ]), 23 | ); 24 | 25 | final app = createLintUsage( 26 | name: 'test_app', 27 | plugins: {'test_lint': plugin.uri}, 28 | source: {'lib/main.dart': 'void fn() {}'}, 29 | ); 30 | 31 | final process = Process.runSync( 32 | 'dart', 33 | [customLintBinPath], 34 | workingDirectory: app.path, 35 | stdoutEncoding: utf8, 36 | stderrEncoding: utf8, 37 | ); 38 | 39 | expect( 40 | trimDependencyOverridesWarning(process.stderr), 41 | startsWith(''' 42 | Plugin hello_world threw while analyzing ${app.file('lib', 'main.dart').resolveSymbolicLinksSync()}: 43 | Bad state: hello 44 | #0 hello_world.startUp (package:test_lint/test_lint.dart:'''), 45 | ); 46 | expect(process.stdout, isEmpty); 47 | expect(process.exitCode, 1); 48 | }); 49 | 50 | test('inside post-run callbacks', () { 51 | final plugin = createPlugin( 52 | name: 'test_lint', 53 | main: createPluginSource([ 54 | TestLintRule( 55 | code: 'hello_world', 56 | message: 'Hello world', 57 | startUp: ''' 58 | context.addPostRunCallback(() { 59 | throw StateError('hello'); 60 | }); 61 | context.addPostRunCallback(() { 62 | throw StateError('hello2'); 63 | }); 64 | return super.startUp(resolver, context); 65 | ''', 66 | ), 67 | ]), 68 | ); 69 | 70 | final app = createLintUsage( 71 | name: 'test_app', 72 | plugins: {'test_lint': plugin.uri}, 73 | source: {'lib/main.dart': 'void fn() {}'}, 74 | ); 75 | 76 | final process = Process.runSync( 77 | 'dart', 78 | [customLintBinPath], 79 | workingDirectory: app.path, 80 | stdoutEncoding: utf8, 81 | stderrEncoding: utf8, 82 | ); 83 | 84 | expect( 85 | trimDependencyOverridesWarning(process.stderr), 86 | allOf( 87 | contains( 88 | ''' 89 | Bad state: hello 90 | #0 hello_world.startUp. (package:test_lint/test_lint.dart:''', 91 | ), 92 | contains( 93 | ''' 94 | Bad state: hello2 95 | #0 hello_world.startUp. (package:test_lint/test_lint.dart:''', 96 | ), 97 | ), 98 | ); 99 | expect(process.stdout, ''' 100 | Analyzing... 101 | 102 | lib/main.dart:1:6 • Hello world • hello_world • INFO 103 | 104 | 1 issue found. 105 | '''); 106 | expect(process.exitCode, 1); 107 | }); 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/add_ignore.diff: -------------------------------------------------------------------------------- 1 | Message: `Ignore "hello_world" for line` 2 | Priority: 1 3 | Diff for file `lib/main.dart:1`: 4 | ``` 5 | - void fn() {} 6 | + // ignore: hello_world 7 | + void fn() {} 8 | 9 | void fn2() {} 10 | ``` 11 | --- 12 | Message: `Ignore "hello_world" for file` 13 | Priority: 0 14 | Diff for file `lib/main.dart:1`: 15 | ``` 16 | - void fn() {} 17 | + // ignore_for_file: hello_world 18 | + void fn() {} 19 | 20 | void fn2() {} 21 | ``` 22 | --- 23 | Message: `Ignore "hello_world" for line` 24 | Priority: 1 25 | Diff for file `lib/main.dart:3`: 26 | ``` 27 | void fn() {} 28 | 29 | - void fn2() {} 30 | + // ignore: hello_world 31 | + void fn2() {} 32 | 33 | void fn3() {} 34 | ``` 35 | --- 36 | Message: `Ignore "hello_world" for file` 37 | Priority: 0 38 | Diff for file `lib/main.dart:1`: 39 | ``` 40 | - void fn() {} 41 | + // ignore_for_file: hello_world 42 | + void fn() {} 43 | 44 | void fn2() {} 45 | ``` 46 | --- 47 | Message: `Ignore "hello_world" for line` 48 | Priority: 1 49 | Diff for file `lib/main.dart:5`: 50 | ``` 51 | void fn2() {} 52 | 53 | - void fn3() {} 54 | + // ignore: hello_world 55 | + void fn3() {} 56 | ``` 57 | --- 58 | Message: `Ignore "hello_world" for file` 59 | Priority: 0 60 | Diff for file `lib/main.dart:1`: 61 | ``` 62 | - void fn() {} 63 | + // ignore_for_file: hello_world 64 | + void fn() {} 65 | 66 | void fn2() {} 67 | ``` 68 | --- 69 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/fixes.diff: -------------------------------------------------------------------------------- 1 | Message: `Fix hello_world` 2 | Priority: 1 3 | Diff for file `lib/main.dart:1`: 4 | ``` 5 | - void fn() {} 6 | + void fnfixed() {} 7 | 8 | void fn2() {} 9 | ``` 10 | --- 11 | Message: `Fix all "hello_world"` 12 | Priority: 0 13 | Diff for file `lib/main.dart:1`: 14 | ``` 15 | - void fn() {} 16 | - 17 | - void fn2() {} 18 | + void fnfixed() {} 19 | + 20 | + void fn2fixed() {} 21 | ``` 22 | --- 23 | Message: `Fix hello_world` 24 | Priority: 1 25 | Diff for file `lib/main.dart:3`: 26 | ``` 27 | void fn() {} 28 | 29 | - void fn2() {} 30 | + void fn2fixed() {} 31 | ``` 32 | --- 33 | Message: `Fix all "hello_world"` 34 | Priority: 0 35 | Diff for file `lib/main.dart:1`: 36 | ``` 37 | - void fn() {} 38 | - 39 | - void fn2() {} 40 | + void fnfixed() {} 41 | + 42 | + void fn2fixed() {} 43 | ``` 44 | --- 45 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/multi_change.diff: -------------------------------------------------------------------------------- 1 | Message: `Fix hello_world` 2 | Priority: 1 3 | Diff for file `lib/main.dart:1`: 4 | ``` 5 | - void fn() {} 6 | + void fnfixedfixed() {} 7 | 8 | void fn2() {} 9 | ``` 10 | Diff for file `lib/src/hello_world.dart:1`: 11 | ``` 12 | - void fn() {} 13 | + void fnfixedfixed() {} 14 | 15 | void fn2() {} 16 | ``` 17 | --- 18 | Message: `Fix hello_world` 19 | Priority: 1 20 | Diff for file `lib/main.dart:3`: 21 | ``` 22 | void fn() {} 23 | 24 | - void fn2() {} 25 | + void fn2fixedfixed() {} 26 | ``` 27 | Diff for file `lib/src/hello_world.dart:3`: 28 | ``` 29 | void fn() {} 30 | 31 | - void fn2() {} 32 | + void fn2fixedfixed() {} 33 | ``` 34 | --- 35 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/no_change.diff: -------------------------------------------------------------------------------- 1 | Message: `Fix hello_world` 2 | Priority: 1 3 | --- 4 | Message: `Fix all "hello_world"` 5 | Priority: 0 6 | --- 7 | Message: `Fix hello_world` 8 | Priority: 1 9 | --- 10 | Message: `Fix all "hello_world"` 11 | Priority: 0 12 | --- 13 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/silenced_change.diff: -------------------------------------------------------------------------------- 1 | Message: `Fix hello_world` 2 | Priority: 1 3 | Diff for file `lib/main.dart:1`: 4 | ``` 5 | - void fn() {} 6 | + void fnfixed() {} 7 | 8 | void fn2() {} 9 | ``` 10 | --- 11 | Message: `Fix all "hello_world"` 12 | Priority: 0 13 | Diff for file `lib/main.dart:1`: 14 | ``` 15 | - void fn() {} 16 | - 17 | - void fn2() {} 18 | + void fnfixed() {} 19 | + 20 | + void fn2fixed() {} 21 | 22 | // ignore: hello_world 23 | ``` 24 | --- 25 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/single_fix.diff: -------------------------------------------------------------------------------- 1 | Message: `Fix hello_world` 2 | Priority: 1 3 | Diff for file `lib/main.dart:1`: 4 | ``` 5 | - void fn() {} 6 | + void fnfixed() {} 7 | ``` 8 | --- 9 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/fixes/update_ignore.diff: -------------------------------------------------------------------------------- 1 | Message: `Ignore "hello_world" for line` 2 | Priority: 1 3 | Diff for file `lib/main.dart:5`: 4 | ``` 5 | 6 | // ignore_for_file: foo 7 | - // ignore: foo 8 | + // ignore: foo, hello_world 9 | void fn2() {} 10 | ``` 11 | --- 12 | Message: `Ignore "hello_world" for file` 13 | Priority: 0 14 | Diff for file `lib/main.dart:4`: 15 | ``` 16 | void fn() {} 17 | 18 | - // ignore_for_file: foo 19 | + // ignore_for_file: foo, hello_world 20 | // ignore: foo 21 | void fn2() {} 22 | ``` 23 | --- 24 | -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/ignore_quick_fix.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "priority": 1, 4 | "change": { 5 | "message": "Ignore \"hello_world\" for line", 6 | "edits": [ 7 | { 8 | "fileStamp": 0, 9 | "edits": [ 10 | { 11 | "offset": 0, 12 | "length": 0, 13 | "replacement": "// ignore: hello_world\n" 14 | } 15 | ] 16 | } 17 | ], 18 | "linkedEditGroups": [], 19 | "id": "<>" 20 | } 21 | }, 22 | { 23 | "priority": 0, 24 | "change": { 25 | "message": "Ignore \"hello_world\" for file", 26 | "edits": [ 27 | { 28 | "fileStamp": 0, 29 | "edits": [ 30 | { 31 | "offset": 0, 32 | "length": 0, 33 | "replacement": "// ignore_for_file: hello_world\n" 34 | } 35 | ] 36 | } 37 | ], 38 | "linkedEditGroups": [], 39 | "id": "<>" 40 | } 41 | }, 42 | { 43 | "priority": 1, 44 | "change": { 45 | "message": "Ignore \"foo\" for line", 46 | "edits": [ 47 | { 48 | "fileStamp": 0, 49 | "edits": [ 50 | { 51 | "offset": 0, 52 | "length": 0, 53 | "replacement": "// ignore: foo\n" 54 | } 55 | ] 56 | } 57 | ], 58 | "linkedEditGroups": [], 59 | "id": "<>" 60 | } 61 | }, 62 | { 63 | "priority": 0, 64 | "change": { 65 | "message": "Ignore \"foo\" for file", 66 | "edits": [ 67 | { 68 | "fileStamp": 0, 69 | "edits": [ 70 | { 71 | "offset": 0, 72 | "length": 0, 73 | "replacement": "// ignore_for_file: foo\n" 74 | } 75 | ] 76 | } 77 | ], 78 | "linkedEditGroups": [], 79 | "id": "<>" 80 | } 81 | } 82 | ] -------------------------------------------------------------------------------- /packages/custom_lint/test/goldens/server_test/redirect_logs.golden: -------------------------------------------------------------------------------- 1 | [hello_world] 1990-01-01T00:00:00.000 Hello world 2 | [hello_world] 1990-01-01T00:00:00.000 Plugin hello_world threw while analyzing app/lib/another.dart: 3 | [hello_world] 1990-01-01T00:00:00.000 Bad state: fail 4 | [hello_world] 1990-01-01T00:00:00.000 #0 hello_world.run. (package:test_lint/test_lint.dart:29:3) -------------------------------------------------------------------------------- /packages/custom_lint/test/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | final throwsAssertionError = throwsA(isAssertionError); 6 | 7 | final isAssertionError = isA(); 8 | 9 | Matcher matchesLogGolden( 10 | String goldenPath, { 11 | Map? paths, 12 | }) { 13 | return isA().having( 14 | (e) => _normalizeLog(e, paths: paths), 15 | 'normalized log matches golden file $goldenPath', 16 | _normalizeLog(File(goldenPath).readAsStringSync(), paths: paths), 17 | ); 18 | } 19 | 20 | void saveLogGoldens( 21 | File goldenPath, 22 | String content, { 23 | Map? paths, 24 | }) { 25 | goldenPath.createSync(recursive: true); 26 | goldenPath.writeAsStringSync(_normalizeLog(content, paths: paths)); 27 | } 28 | 29 | final _logDateRegex = RegExp(r'^\[(.+?)\] \S+', multiLine: true); 30 | 31 | String _normalizeLog(String log, {Map? paths}) { 32 | var result = log.replaceAllMapped( 33 | _logDateRegex, 34 | (match) => '[${match.group(1)}] ${DateTime(1990).toIso8601String()}', 35 | ); 36 | 37 | if (paths != null) { 38 | for (final entry in paths.entries) { 39 | result = result.replaceAll(entry.key.toString(), '${entry.value}/'); 40 | result = result.replaceAll(entry.key.toFilePath(), '${entry.value}/'); 41 | } 42 | } 43 | 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /packages/custom_lint/test/mock_fs.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | /// Overrides the body of a test so that I/O is run against an in-memory 6 | /// file system, not the host's disk. 7 | /// 8 | /// The I/O override is applied only to the code running within [testBody]. 9 | Future runWithIOOverride( 10 | FutureOr Function(Stream out, Stream err) testBody, { 11 | Directory? currentDirectory, 12 | bool supportsAnsiEscapes = false, 13 | }) async { 14 | final fs = _MockFs( 15 | stdout, 16 | currentDirectory ?? Directory.current, 17 | supportsAnsiEscapes: supportsAnsiEscapes, 18 | ); 19 | 20 | try { 21 | return await IOOverrides.runWithIOOverrides( 22 | () => testBody(fs.stdout.stream, fs.stderr.stream), 23 | fs, 24 | ); 25 | } finally { 26 | unawaited(fs.stderr.close()); 27 | unawaited(fs.stdout.close()); 28 | } 29 | } 30 | 31 | class _StdoutOverride implements Stdout { 32 | _StdoutOverride( 33 | this._stdout, { 34 | required this.supportsAnsiEscapes, 35 | }); 36 | 37 | final Stdout _stdout; 38 | 39 | final _controller = StreamController(); 40 | 41 | Stream get stream => _controller.stream; 42 | 43 | @override 44 | Encoding get encoding => _stdout.encoding; 45 | 46 | @override 47 | set encoding(Encoding e) => throw UnimplementedError(); 48 | 49 | @override 50 | void add(List data) => throw UnimplementedError(); 51 | 52 | @override 53 | void addError(Object error, [StackTrace? stackTrace]) { 54 | throw UnimplementedError(); 55 | } 56 | 57 | @override 58 | Future addStream(Stream> stream) => 59 | throw UnimplementedError(); 60 | 61 | @override 62 | Future close() => _controller.close(); 63 | 64 | @override 65 | Future get done => throw UnimplementedError(); 66 | 67 | @override 68 | Future flush() => throw UnimplementedError(); 69 | 70 | @override 71 | bool get hasTerminal => _stdout.hasTerminal; 72 | 73 | @override 74 | IOSink get nonBlocking => _stdout.nonBlocking; 75 | 76 | @override 77 | final bool supportsAnsiEscapes; 78 | 79 | @override 80 | int get terminalColumns => _stdout.terminalColumns; 81 | 82 | @override 83 | int get terminalLines => _stdout.terminalLines; 84 | 85 | @override 86 | void write(Object? object) { 87 | _controller.add(object.toString()); 88 | } 89 | 90 | @override 91 | void writeAll(Iterable objects, [String sep = '']) { 92 | _controller.add(objects.join(sep)); 93 | } 94 | 95 | @override 96 | void writeCharCode(int charCode) => throw UnimplementedError(); 97 | 98 | @override 99 | void writeln([Object? object = '']) { 100 | _controller.add('$object\n'); 101 | } 102 | 103 | @override 104 | String get lineTerminator => _stdout.lineTerminator; 105 | 106 | @override 107 | set lineTerminator(String value) => _stdout.lineTerminator = value; 108 | } 109 | 110 | /// Used to override file I/O with an in-memory file system for testing. 111 | /// 112 | /// Usage: 113 | /// 114 | /// ```dart 115 | /// test('My FS test', withMockFs(() { 116 | /// File('foo').createSync(); // File created in memory 117 | /// })); 118 | /// ``` 119 | /// 120 | /// Alternatively, set [IOOverrides.global] to a [_MockFs] instance in your 121 | /// test's `setUp`, and to `null` in the `tearDown`. 122 | class _MockFs extends IOOverrides { 123 | _MockFs( 124 | Stdout out, 125 | this._directory, { 126 | required bool supportsAnsiEscapes, 127 | }) : stdout = _StdoutOverride(out, supportsAnsiEscapes: supportsAnsiEscapes), 128 | stderr = _StdoutOverride(out, supportsAnsiEscapes: supportsAnsiEscapes); 129 | 130 | @override 131 | final _StdoutOverride stdout; 132 | 133 | @override 134 | final _StdoutOverride stderr; 135 | 136 | Directory _directory; 137 | 138 | @override 139 | Directory getCurrentDirectory() => _directory; 140 | 141 | @override 142 | FileSystemEntityType fseGetTypeSync(String path, bool followLinks) { 143 | return Zone.current.parent!.run(() { 144 | // Workaround to https://github.com/dart-lang/sdk/issues/54741 145 | return FileSystemEntity.typeSync(path, followLinks: followLinks); 146 | }); 147 | } 148 | 149 | @override 150 | void setCurrentDirectory(String path) { 151 | _directory = Directory(path); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/custom_lint/test/peer_project_meta.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:path/path.dart'; 5 | 6 | final customLintBinPath = join( 7 | PeerProjectMeta.current.customLintPath, 8 | 'bin', 9 | 'custom_lint.dart', 10 | ); 11 | 12 | class PeerProjectMeta { 13 | PeerProjectMeta({ 14 | required this.customLintPath, 15 | required this.customLintBuilderPath, 16 | required this.customLintCorePath, 17 | required this.exampleAppPath, 18 | required this.exampleLintPath, 19 | required this.exampleLintPackageConfigString, 20 | required this.exampleLintPackageConfig, 21 | }) { 22 | final dirs = [ 23 | customLintPath, 24 | customLintBuilderPath, 25 | exampleAppPath, 26 | exampleLintPath, 27 | ]; 28 | 29 | for (final dir in dirs) { 30 | if (!Directory(dir).existsSync()) { 31 | throw StateError('Nothing found at $dir.'); 32 | } 33 | } 34 | } 35 | 36 | factory PeerProjectMeta.fromDirectory(Directory directory) { 37 | final exampleAppDir = Directory( 38 | join( 39 | directory.path, 40 | 'example', 41 | ), 42 | ); 43 | 44 | final packagesPath = normalize(join(directory.path, '..')); 45 | 46 | final examplePackageConfigPath = join( 47 | normalize(exampleAppDir.path), 48 | 'example_lint', 49 | '.dart_tool', 50 | 'package_config.json', 51 | ); 52 | final exampleLintPackageConfigString = 53 | File(examplePackageConfigPath).readAsStringSync(); 54 | 55 | return PeerProjectMeta( 56 | customLintPath: join(packagesPath, 'custom_lint'), 57 | customLintBuilderPath: join(packagesPath, 'custom_lint_builder'), 58 | customLintCorePath: join(packagesPath, 'custom_lint_core'), 59 | exampleAppPath: normalize(exampleAppDir.path), 60 | exampleLintPath: 61 | join(packagesPath, 'custom_lint', 'example', 'example_lint'), 62 | exampleLintPackageConfigString: exampleLintPackageConfigString, 63 | exampleLintPackageConfig: 64 | jsonDecode(exampleLintPackageConfigString) as Map, 65 | ); 66 | } 67 | 68 | static final current = PeerProjectMeta.fromDirectory(Directory.current); 69 | 70 | final String customLintPath; 71 | final String customLintBuilderPath; 72 | final String customLintCorePath; 73 | final String exampleAppPath; 74 | final String exampleLintPath; 75 | final String exampleLintPackageConfigString; 76 | final Map exampleLintPackageConfig; 77 | } 78 | -------------------------------------------------------------------------------- /packages/custom_lint/test/run_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 5 | import 'package:custom_lint/src/plugin_delegate.dart'; 6 | import 'package:custom_lint/src/runner.dart'; 7 | import 'package:custom_lint/src/server_isolate_channel.dart'; 8 | import 'package:custom_lint/src/v2/custom_lint_analyzer_plugin.dart'; 9 | import 'package:custom_lint/src/workspace.dart'; 10 | import 'package:path/path.dart' as p; 11 | import 'package:test/test.dart'; 12 | 13 | Future> runServerInCliModeForApp( 14 | Directory directory, 15 | 16 | // to ignoreErrors as we cannot explicitly handle errors 17 | ) async { 18 | final runner = await startRunnerForApp(directory, includeBuiltInLints: false); 19 | return runner.runner.getLints(reload: false); 20 | } 21 | 22 | class ManualRunner { 23 | ManualRunner(this.runner, this.channel); 24 | 25 | final CustomLintRunner runner; 26 | final ServerIsolateChannel channel; 27 | 28 | Future get initialize => runner.initialize; 29 | 30 | Future> getLints({required bool reload}) async { 31 | return runner.getLints(reload: reload); 32 | } 33 | 34 | Future getFixes( 35 | String path, 36 | int offset, 37 | ) async { 38 | return runner.getFixes(path, offset); 39 | } 40 | 41 | Future close() async { 42 | await runner.close(); 43 | await channel.close(); 44 | } 45 | } 46 | 47 | Future startRunnerForApp( 48 | Directory directory, { 49 | bool ignoreErrors = false, 50 | bool includeBuiltInLints = true, 51 | bool watchMode = false, 52 | bool fix = false, 53 | }) async { 54 | final zone = Zone.current; 55 | final channel = ServerIsolateChannel(); 56 | 57 | final customLintServer = await CustomLintServer.start( 58 | sendPort: channel.receivePort.sendPort, 59 | workingDirectory: directory, 60 | fix: fix, 61 | delegate: CommandCustomLintDelegate(), 62 | includeBuiltInLints: includeBuiltInLints, 63 | watchMode: watchMode, 64 | ); 65 | 66 | return CustomLintServer.runZoned(() => customLintServer, () async { 67 | final workspace = await CustomLintWorkspace.fromPaths( 68 | [directory.path], 69 | workingDirectory: directory, 70 | ); 71 | final runner = CustomLintRunner(customLintServer, workspace, channel); 72 | addTearDown(runner.close); 73 | 74 | if (!ignoreErrors) { 75 | runner.channel 76 | ..responseErrors.listen((event) { 77 | zone.handleUncaughtError( 78 | TestFailure( 79 | '${event.message} ${event.code}\n${event.stackTrace}', 80 | ), 81 | StackTrace.current, 82 | ); 83 | }) 84 | ..pluginErrors.listen((event) { 85 | zone.handleUncaughtError( 86 | TestFailure('${event.message}\n${event.stackTrace}'), 87 | StackTrace.current, 88 | ); 89 | }); 90 | } 91 | 92 | unawaited(runner.initialize); 93 | 94 | return ManualRunner(runner, channel); 95 | }); 96 | } 97 | 98 | extension LogFile on Directory { 99 | File get log { 100 | return File(p.join(path, 'custom_lint.log')); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/custom_lint/tools/analyzer_plugin/bin/plugin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:custom_lint/src/analyzer_plugin_starter.dart'; 4 | 5 | Future main(List args, SendPort sendPort) async { 6 | await start(args, sendPort); 7 | } 8 | -------------------------------------------------------------------------------- /packages/custom_lint/tools/analyzer_plugin/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_analyzer_plugin_loader 2 | description: This pubspec determines the version of the analyzer plugin to load. 3 | version: 0.7.0 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | custom_lint: 0.7.5 11 | 12 | # TODO: If you want to contribute to custom_lint, add a pubspec_overrides.yaml file 13 | # in this folder, containing the following: 14 | # dependency_overrides: 15 | # custom_lint: 16 | # path: /absolute/path/to/custom_lint/folder 17 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/README.md: -------------------------------------------------------------------------------- 1 |

2 |

custom_lint_builder

3 | An package for defining custom lints. 4 |

5 | 6 |

7 | License 8 |

9 | 10 | ## About 11 | 12 | `custom_lint_builder` is a package that should be associated with [custom_lint] 13 | for defining custom_lint plugins. 14 | 15 | 16 | If a package wants to access classes such as `LintRule` or `Assist` but do 17 | not want to make a custom_lint plugin (such as for exposing new utilities 18 | for plugin authors), then use `custom_lint_core` instead. 19 | 20 | Using `custom_lint_builder` is reserved to plugin authors. Depending it on it 21 | will tell custom_lint that your package is a plugin, and therefore will try to 22 | run it. 23 | 24 | See [custom_lint] for more informations 25 | 26 | --- 27 | 28 |

29 | 30 | 31 | 32 |

33 | Built and maintained by Invertase. 34 |

35 |

36 | 37 | [custom_lint]: https://github.com/invertase/dart_custom_lint 38 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/README.md: -------------------------------------------------------------------------------- 1 | # Custom Lint Example 2 | 3 | A simple example how powerful is custom_lint package. 4 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../../../analysis_options.yaml 2 | 3 | analyzer: 4 | plugins: 5 | - custom_lint 6 | 7 | linter: 8 | rules: 9 | public_member_api_docs: false 10 | avoid_print: false 11 | unreachable_from_main: false 12 | 13 | custom_lint: 14 | rules: 15 | - prefer_lint 16 | - map: false 17 | # - riverpod_final_provider: false 18 | - map2: 19 | length: 42 20 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/example.md: -------------------------------------------------------------------------------- 1 | # Custom Lint Example 2 | 3 | A simple example how powerful is custom_lint package. 4 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/example_lint/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # include: ../analysis_options.yaml 2 | 3 | # linter: 4 | # rules: 5 | # public_member_api_docs: false 6 | # avoid_print: false 7 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/example_lint/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_builder_example_lint 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=3.0.0 <4.0.0' 6 | 7 | dependencies: 8 | analyzer: ^7.0.0 9 | analyzer_plugin: ^0.13.0 10 | custom_lint_builder: 11 | path: ../../../custom_lint_builder 12 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/example_lint/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_core,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: ../../../custom_lint 5 | custom_lint_builder: 6 | path: ../.. 7 | custom_lint_core: 8 | path: ../../../custom_lint_core 9 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod/riverpod.dart'; 2 | 3 | void main() { 4 | print('hello world'); 5 | } 6 | 7 | class Main {} 8 | 9 | // expect_lint: riverpod_final_provider 10 | ProviderBase provider = Provider((ref) => 0); 11 | 12 | // expect_lint: riverpod_final_provider 13 | Provider provider2 = Provider((ref) => 0); 14 | 15 | Object? foo = 42; 16 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_builder_example_app 2 | publish_to: none 3 | 4 | environment: 5 | sdk: '>=3.0.0 <4.0.0' 6 | 7 | dependencies: 8 | riverpod: ^2.0.0 9 | 10 | dev_dependencies: 11 | custom_lint: 12 | custom_lint_builder_example_lint: 13 | path: ./example_lint 14 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/example/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint,custom_lint_builder,custom_lint_builder_example_lint,custom_lint_core,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: ../../custom_lint 5 | custom_lint_builder: 6 | path: .. 7 | custom_lint_builder_example_lint: 8 | path: example_lint 9 | custom_lint_core: 10 | path: ../../custom_lint_core 11 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/lib/custom_lint_builder.dart: -------------------------------------------------------------------------------- 1 | export 'package:custom_lint_core/custom_lint_core.dart'; 2 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/lib/src/channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:isolate'; 4 | 5 | import 'package:analyzer_plugin/protocol/protocol.dart'; 6 | // ignore: implementation_imports, tight versioning with custom_lint 7 | import 'package:custom_lint/src/async_operation.dart'; 8 | // ignore: implementation_imports, tight versioning with custom_lint 9 | import 'package:custom_lint/src/channels.dart'; 10 | // ignore: implementation_imports, tight versioning with custom_lint 11 | import 'package:custom_lint/src/v2/protocol.dart'; 12 | import 'package:custom_lint_core/custom_lint_core.dart'; 13 | 14 | import 'client.dart'; 15 | 16 | /// Converts a Stream/Sink into a SendPort/ReceivePort equivalent 17 | class StreamToSentPortAdapter { 18 | /// Converts a Stream/Sink into a SendPort/ReceivePort equivalent 19 | StreamToSentPortAdapter( 20 | Stream> input, 21 | void Function(Map output) output, { 22 | required void Function() onDone, 23 | }) { 24 | final Stream outputStream = _outputReceivePort.asBroadcastStream(); 25 | final inputSendPort = 26 | outputStream.where((e) => e is SendPort).cast().safeFirst; 27 | 28 | final sub = outputStream 29 | .where((e) => e is! SendPort) 30 | .map((e) => e! as Map) 31 | .listen((e) => output(e)); 32 | 33 | input.listen( 34 | (e) { 35 | unawaited(inputSendPort.then((value) => value.send(e))); 36 | }, 37 | onDone: () { 38 | _outputReceivePort.close(); 39 | unawaited(sub.cancel()); 40 | onDone(); 41 | }, 42 | ); 43 | } 44 | 45 | /// The [SendPort] associated with the input [Stream]. 46 | SendPort get sendPort => _outputReceivePort.sendPort; 47 | 48 | // TODO appears to sometime not be closed. 49 | // Could be because of the `onDone` callback not being invoked. 50 | final _outputReceivePort = ReceivePort(); 51 | } 52 | 53 | /// The prototype of plugin's `createPlugin` entrypoint function. 54 | typedef CreatePluginMain = PluginBase Function(); 55 | 56 | /// Starts a custom_lint client using web sockets. 57 | Future runSocket( 58 | Map pluginMains, { 59 | required int port, 60 | required String host, 61 | required bool fix, 62 | required bool includeBuiltInLints, 63 | }) async { 64 | late Future client; 65 | 66 | await asyncRunZonedGuarded( 67 | () => client = Future(() async { 68 | // ignore: close_sinks, connection stays open until the plugin is killed 69 | final socket = await Socket.connect(host, port); 70 | final socketChannel = JsonSocketChannel(Future.value(socket)); 71 | final registeredPlugins = {}; 72 | 73 | for (final main in pluginMains.entries) { 74 | Zone.current.runGuarded( 75 | () => registeredPlugins[main.key] = main.value(), 76 | ); 77 | } 78 | 79 | return CustomLintPluginClient( 80 | includeBuiltInLints: includeBuiltInLints, 81 | fix: fix, 82 | _SocketCustomLintClientChannel( 83 | socketChannel, 84 | registeredPlugins, 85 | onDone: () { 86 | // If the server somehow quit, forcibly stop the client. 87 | // In theory it should stop naturally, but let's make sure of this to prevent leaks. 88 | // Tried with `socket.done.then` but it somehow was never invoked 89 | exit(0); 90 | }, 91 | ), 92 | ); 93 | }), 94 | (error, stackTrace) { 95 | unawaited(client.then((value) => value.handleError(error, stackTrace))); 96 | }, 97 | zoneSpecification: ZoneSpecification( 98 | print: (self, parent, zone, line) { 99 | unawaited(client.then((value) => value.handlePrint(line))); 100 | }, 101 | ), 102 | ); 103 | } 104 | 105 | /// An interface for clients to send messages to the custom_lint server. 106 | abstract class CustomLintClientChannel { 107 | /// An interface for clients to send messages to the custom_lint server. 108 | CustomLintClientChannel(this.registeredPlugins); 109 | 110 | /// The [SendPort] that will be passed to analyzer_plugin 111 | SendPort get sendPort; 112 | 113 | /// The list of plugins installed by custom_lint. 114 | final Map registeredPlugins; 115 | 116 | /// Messages from the custom_lint server 117 | Stream get input; 118 | 119 | void _sendJson(Map json); 120 | 121 | /// Sends a response to the custom_lint server, associated to a request 122 | void sendResponse(CustomLintResponse response) { 123 | _sendJson(CustomLintMessage.response(response).toJson()); 124 | } 125 | 126 | /// Sends a notification to the custom_lint server, which is not associated with 127 | /// a request. 128 | void sendEvent(CustomLintEvent event) { 129 | _sendJson(CustomLintMessage.event(event).toJson()); 130 | } 131 | } 132 | 133 | class _SocketCustomLintClientChannel extends CustomLintClientChannel { 134 | _SocketCustomLintClientChannel( 135 | this.socket, 136 | super.registeredPlugins, { 137 | required this.onDone, 138 | }); 139 | 140 | @override 141 | SendPort get sendPort => _adapter.sendPort; 142 | 143 | final void Function() onDone; 144 | final JsonSocketChannel socket; 145 | 146 | late final StreamToSentPortAdapter _adapter = StreamToSentPortAdapter( 147 | onDone: onDone, 148 | input 149 | .where((e) => e is CustomLintRequestAnalyzerPluginRequest) 150 | .cast() 151 | .map((event) => event.request.toJson()), 152 | (analyzerPluginOutput) { 153 | if (analyzerPluginOutput.containsKey(Notification.EVENT)) { 154 | sendEvent( 155 | CustomLintEvent.analyzerPluginNotification( 156 | Notification.fromJson(analyzerPluginOutput), 157 | ), 158 | ); 159 | } else { 160 | final response = Response.fromJson(analyzerPluginOutput); 161 | sendResponse( 162 | CustomLintResponse.analyzerPluginResponse(response, id: response.id), 163 | ); 164 | } 165 | }, 166 | ); 167 | 168 | @override 169 | late final Stream input = socket.messages 170 | .cast>() 171 | .map(CustomLintRequest.fromJson) 172 | .asBroadcastStream(); 173 | 174 | @override 175 | void _sendJson(Map json) { 176 | unawaited(socket.sendJson(json)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/lib/src/expect_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/error/error.dart' 2 | hide 3 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 4 | LintCode; 5 | import 'package:analyzer/error/listener.dart'; 6 | import 'package:analyzer/source/line_info.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | import '../custom_lint_builder.dart'; 10 | 11 | final _expectLintRegex = RegExp(r'//\s*expect_lint\s*:(.+)$', multiLine: true); 12 | 13 | /// A class implementing the logic for `// expect_lint: code` comments 14 | @internal 15 | class ExpectLint { 16 | /// A class implementing the logic for `// expect_lint: code` comments 17 | const ExpectLint(this.analysisErrors); 18 | 19 | static const _code = LintCode( 20 | name: 'unfulfilled_expect_lint', 21 | problemMessage: 22 | 'Expected to find the lint {0} on next line but none found.', 23 | correctionMessage: 'Either update the code such that it emits the lint {0} ' 24 | 'or update the expect_lint clause to not include the code {0}.', 25 | errorSeverity: ErrorSeverity.ERROR, 26 | ); 27 | 28 | /// The list of lints emitted in the file. 29 | final List analysisErrors; 30 | 31 | /// Emits expect_lints 32 | void run( 33 | CustomLintResolver resolver, 34 | ErrorReporter reporter, 35 | ) { 36 | final expectLints = _getAllExpectedLints( 37 | resolver.source.contents.data, 38 | resolver.lineInfo, 39 | filePath: resolver.path, 40 | ); 41 | 42 | final allExpectedLints = expectLints 43 | .map((e) => _ComparableExpectLintMeta(e.line, e.code)) 44 | .toSet(); 45 | 46 | // The list of all the expect_lints codes that don't have a matching lint. 47 | final unfulfilledExpectedLints = expectLints.toList(); 48 | 49 | for (final lint in analysisErrors) { 50 | final lintLine = resolver.lineInfo.getLocation(lint.offset); 51 | 52 | final matchingExpectLintMeta = _ComparableExpectLintMeta( 53 | // Lints use 1-based offsets but expectLints use 0-based offsets. So 54 | // we remove 1 to have them on the same unit. Then we remove 1 again 55 | // to access the line before the lint. 56 | lintLine.lineNumber - 2, 57 | lint.errorCode.name, 58 | ); 59 | 60 | if (allExpectedLints.contains(matchingExpectLintMeta)) { 61 | // The lint has a matching expect_lint. Let's ignore the lint and mark 62 | // the associated expect_lint as fulfilled. 63 | unfulfilledExpectedLints.removeWhere( 64 | (e) => 65 | e.line == matchingExpectLintMeta.line && 66 | e.code == matchingExpectLintMeta.code, 67 | ); 68 | } else { 69 | // The lint has no matching expect_lint. Therefore we let it propagate 70 | reporter.reportError(lint); 71 | } 72 | } 73 | 74 | // Some expect_lint clauses where not respected 75 | for (final unfulfilledExpectedLint in unfulfilledExpectedLints) { 76 | reporter.atOffset( 77 | errorCode: _code, 78 | offset: unfulfilledExpectedLint.offset, 79 | length: unfulfilledExpectedLint.code.length, 80 | arguments: [unfulfilledExpectedLint.code], 81 | ); 82 | } 83 | } 84 | 85 | List<_ExpectLintMeta> _getAllExpectedLints( 86 | String source, 87 | LineInfo lineInfo, { 88 | required String filePath, 89 | }) { 90 | // expect_lint is only supported in dart files as it relies on dart comments 91 | if (!filePath.endsWith('.dart')) return const []; 92 | 93 | final expectLints = _expectLintRegex.allMatches(source); 94 | 95 | return expectLints.expand((expectLint) { 96 | final lineNumber = lineInfo.getLocation(expectLint.start).lineNumber - 1; 97 | final codesStartOffset = source.indexOf(':', expectLint.start) + 1; 98 | 99 | final codes = expectLint.group(1)!.split(','); 100 | var codeOffsetAcc = codesStartOffset; 101 | 102 | return codes.map((rawCode) { 103 | final codeOffset = 104 | codeOffsetAcc + (rawCode.length - rawCode.trimLeft().length); 105 | codeOffsetAcc += rawCode.length + 1; 106 | 107 | final code = rawCode.trim(); 108 | 109 | return _ExpectLintMeta( 110 | line: lineNumber, 111 | code: code, 112 | offset: codeOffset, 113 | ); 114 | }); 115 | }).toList(); 116 | } 117 | } 118 | 119 | /// Information about an `// expect_lint: code` clause 120 | @immutable 121 | class _ExpectLintMeta { 122 | /// Information about an `// expect_lint: code` clause 123 | const _ExpectLintMeta({ 124 | required this.line, 125 | required this.code, 126 | required this.offset, 127 | }) : assert(line >= 0, 'line must be positive'); 128 | 129 | /// A 0-based offset of the line having the expect_lint clause. 130 | final int line; 131 | 132 | /// The code expected. 133 | final String code; 134 | 135 | /// The index of the first character of [code] within the analyzed file. 136 | final int offset; 137 | } 138 | 139 | @immutable 140 | class _ComparableExpectLintMeta { 141 | const _ComparableExpectLintMeta(this.line, this.code); 142 | 143 | final int line; 144 | final String code; 145 | 146 | @override 147 | int get hashCode => Object.hash(line, code); 148 | 149 | @override 150 | bool operator ==(Object other) { 151 | return other is _ComparableExpectLintMeta && 152 | other.code == code && 153 | other.line == line; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/lib/src/ignore.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/error/error.dart' 2 | hide 3 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 4 | LintCode; 5 | import 'package:collection/collection.dart'; 6 | import 'package:custom_lint_core/custom_lint_core.dart'; 7 | 8 | /// Metadata about ignores at a given line. 9 | class IgnoreMetadata { 10 | IgnoreMetadata._( 11 | this._codes, { 12 | required this.startOffset, 13 | required this.endOffset, 14 | }); 15 | 16 | factory IgnoreMetadata._parse( 17 | RegExpMatch? ignore, { 18 | required int startOffset, 19 | }) { 20 | if (ignore == null) return IgnoreMetadata.empty; 21 | 22 | final fullMatchString = ignore.group(0)!; 23 | 24 | final codes = fullMatchString 25 | .substring(fullMatchString.indexOf(':') + 1) 26 | .split(',') 27 | .map((e) => e.trim()) 28 | .toSet(); 29 | 30 | return IgnoreMetadata._( 31 | codes, 32 | startOffset: ignore.start + startOffset, 33 | endOffset: startOffset + ignore.end, 34 | ); 35 | } 36 | 37 | /// Empty metadata. 38 | static final empty = IgnoreMetadata._( 39 | null, 40 | startOffset: -1, 41 | endOffset: -1, 42 | ); 43 | 44 | final Set? _codes; 45 | 46 | /// Whether there are any ignores for this line. 47 | bool get hasIgnore => _codes != null; 48 | 49 | /// Whether all lints are ignored using `type=lint` 50 | // ignore: use_if_null_to_convert_nulls_to_bools 51 | bool get disablesAllLints => _codes?.contains('type=lint') == true; 52 | 53 | /// The offset of where the ignore starts. 54 | /// 55 | /// Will be -1 if there is no ignore. 56 | final int startOffset; 57 | 58 | /// The offset of where the ignore ends. 59 | final int endOffset; 60 | 61 | /// Whether the given code is ignored. 62 | bool isIgnored(String code) { 63 | final codes = _codes; 64 | if (codes == null) return false; 65 | return codes.contains(code) || disablesAllLints; 66 | } 67 | } 68 | 69 | final _ignoreRegex = RegExp(r'//\s*ignore\s*:.*?$', multiLine: true); 70 | 71 | /// Searches for `// ignore:` matching a given line. 72 | IgnoreMetadata parseIgnoreForLine( 73 | int offset, 74 | CustomLintResolver resolver, 75 | ) { 76 | final line = resolver.lineInfo.getLocation(offset).lineNumber - 1; 77 | 78 | if (line <= 0) return IgnoreMetadata.empty; 79 | 80 | final previousLineOffset = resolver.lineInfo.getOffsetOfLine(line - 1); 81 | final previousLine = resolver.source.contents.data.substring( 82 | previousLineOffset, 83 | offset - 1, 84 | ); 85 | 86 | final codeContent = _ignoreRegex.firstMatch(previousLine); 87 | if (codeContent == null) return IgnoreMetadata.empty; 88 | 89 | return IgnoreMetadata._parse(codeContent, startOffset: previousLineOffset); 90 | } 91 | 92 | final _ignoreForFileRegex = 93 | RegExp(r'//\s*ignore_for_file\s*:.*$', multiLine: true); 94 | 95 | /// Searches for `// ignore_for_file:` in a given file. 96 | List parseIgnoreForFile(String source) { 97 | final ignoreForFiles = _ignoreForFileRegex.allMatches(source).nonNulls; 98 | 99 | return ignoreForFiles 100 | .map((e) => IgnoreMetadata._parse(e, startOffset: 0)) 101 | .toList(); 102 | } 103 | 104 | /// Built in fix to ignore a lint. 105 | class IgnoreCode extends DartFix { 106 | /// The code for 'ignore for line' fix. 107 | static const ignoreId = '<>'; 108 | 109 | @override 110 | String get id => ignoreId; 111 | 112 | @override 113 | void run( 114 | CustomLintResolver resolver, 115 | ChangeReporter reporter, 116 | CustomLintContext context, 117 | AnalysisError analysisError, 118 | List others, 119 | ) { 120 | final ignoreForLine = parseIgnoreForLine(analysisError.offset, resolver); 121 | final ignoreForFile = parseIgnoreForFile(resolver.source.contents.data); 122 | 123 | final ignoreForLineChangeBuilder = reporter.createChangeBuilder( 124 | message: 'Ignore "${analysisError.errorCode.name}" for line', 125 | priority: 1, 126 | ); 127 | 128 | ignoreForLineChangeBuilder.addDartFileEdit((builder) { 129 | if (ignoreForLine.hasIgnore) { 130 | builder.addSimpleInsertion( 131 | ignoreForLine.endOffset, 132 | ', ${analysisError.errorCode.name}', 133 | ); 134 | } else { 135 | final offsetLine = 136 | resolver.lineInfo.getLocation(analysisError.offset).lineNumber - 1; 137 | 138 | final startLineOffset = resolver.lineInfo.getOffsetOfLine(offsetLine); 139 | 140 | final indentLength = resolver.source.contents.data 141 | .substring(startLineOffset) 142 | .indexOf(RegExp(r'\S')); 143 | 144 | builder.addSimpleInsertion( 145 | startLineOffset, 146 | '${' ' * indentLength}// ignore: ${analysisError.errorCode.name}\n', 147 | ); 148 | } 149 | }); 150 | 151 | final ignoreForFileChangeBuilder = reporter.createChangeBuilder( 152 | message: 'Ignore "${analysisError.errorCode.name}" for file', 153 | priority: 0, 154 | ); 155 | 156 | ignoreForFileChangeBuilder.addDartFileEdit((builder) { 157 | final firstIgnore = ignoreForFile.firstOrNull; 158 | if (firstIgnore == null) { 159 | builder.addSimpleInsertion( 160 | 0, 161 | '// ignore_for_file: ${analysisError.errorCode.name}\n', 162 | ); 163 | } else { 164 | builder.addSimpleInsertion( 165 | firstIgnore.endOffset, 166 | ', ${analysisError.errorCode.name}', 167 | ); 168 | } 169 | }); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/lib/src/pragrams.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Alias for vm:prefer-inline 4 | @internal 5 | const preferInline = pragma('vm:prefer-inline'); 6 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_builder 2 | version: 0.7.5 3 | description: A package to help writing custom linters 4 | repository: https://github.com/invertase/dart_custom_lint 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | analyzer: ^7.0.0 11 | analyzer_plugin: ^0.13.0 12 | collection: ^1.16.0 13 | # Using tight constraints as custom_lint_builder communicate with each-other 14 | # using a specific contract 15 | custom_lint: 0.7.5 16 | # Using tight constraints as custom_lint_builder communicate with each-other 17 | # using a specific contract 18 | custom_lint_core: 0.7.5 19 | # Using loose constraint to support a range of analyzer versions. 20 | custom_lint_visitor: ^1.0.0 21 | glob: ^2.1.1 22 | hotreloader: ">=3.0.5 <5.0.0" 23 | meta: ^1.7.0 24 | package_config: ^2.1.0 25 | path: ^1.8.0 26 | pubspec_parse: ^1.2.0 27 | rxdart: ^0.28.0 28 | 29 | dev_dependencies: 30 | test: ^1.22.2 31 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: ../custom_lint 5 | custom_lint_core: 6 | path: ../custom_lint_core 7 | lint_visitor_generator: 8 | path: ../lint_visitor_generator 9 | -------------------------------------------------------------------------------- /packages/custom_lint_builder/test/analyzer_converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/error/error.dart' 2 | hide 3 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 4 | LintCode; 5 | import 'package:analyzer/file_system/memory_file_system.dart'; 6 | import 'package:analyzer/source/file_source.dart'; 7 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 8 | import 'package:custom_lint_builder/src/custom_analyzer_converter.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | test('Converts LintCode', () { 13 | final resourceProvider = MemoryResourceProvider(); 14 | final source = FileSource( 15 | resourceProvider.newFile( 16 | '/home/user/project/lib/main.dart', 17 | 'void main() {}', 18 | ), 19 | ); 20 | final source2 = FileSource( 21 | resourceProvider.newFile( 22 | '/home/user/project/lib/main2.dart', 23 | 'void main2() {}', 24 | ), 25 | ); 26 | 27 | final another = AnalysisError.tmp( 28 | source: source, 29 | offset: 11, 30 | length: 12, 31 | errorCode: const LintCode( 32 | name: 'another', 33 | problemMessage: 'another message', 34 | url: 'https://dart.dev/diagnostics/another', 35 | ), 36 | ); 37 | 38 | expect( 39 | CustomAnalyzerConverter() 40 | .convertAnalysisError( 41 | AnalysisError.tmp( 42 | source: source2, 43 | offset: 13, 44 | length: 14, 45 | errorCode: const LintCode( 46 | name: 'foo', 47 | problemMessage: 'bar', 48 | url: 'https://google.com/diagnostics/foo', 49 | ), 50 | contextMessages: [another.problemMessage], 51 | ), 52 | ) 53 | .toJson(), 54 | { 55 | 'severity': 'INFO', 56 | 'type': 'LINT', 57 | 'location': { 58 | 'file': '/home/user/project/lib/main2.dart', 59 | 'offset': 13, 60 | 'length': 14, 61 | 'startLine': -1, 62 | 'startColumn': -1, 63 | 'endLine': -1, 64 | 'endColumn': -1, 65 | }, 66 | 'message': 'bar', 67 | 'code': 'foo', 68 | 'url': 'https://google.com/diagnostics/foo', 69 | 'contextMessages': [ 70 | { 71 | 'message': 'another message', 72 | 'location': { 73 | 'file': '/home/user/project/lib/main.dart', 74 | 'offset': 11, 75 | 'length': 12, 76 | 'startLine': -1, 77 | 'startColumn': -1, 78 | 'endLine': -1, 79 | 'endColumn': -1, 80 | }, 81 | } 82 | ], 83 | }, 84 | ); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /packages/custom_lint_core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.7.2 - 2025-02-27 2 | 3 | Fix inconsistent version 4 | 5 | ## 0.7.1 - 2025-01-08 6 | 7 | - Support analyzer 7.0.0 8 | 9 | ## 0.7.0 - 2024-10-27 10 | 11 | - `custom_lint --fix` and the generated "Fix all " assists 12 | now correctly handle imports. 13 | - Now supports a broad number of analyzer version. 14 | 15 | ## 0.6.10 - 2024-10-10 16 | 17 | - Added support for `dart:io` imports when using `TypeChecker.fromPackage` (thanks to @oskar-zeinomahmalat-sonarsource) 18 | 19 | ## 0.6.9 - 2024-10-09 20 | 21 | - Support analyzer 0.6.9 22 | 23 | ## 0.6.5 - 2024-08-15 24 | 25 | - Upgraded to analyzer ^6.6.0. 26 | This is a quick fix to unblock the stable Flutter channel. 27 | A more robust fix will come later. 28 | - Fixed a bug where isSuperTypeOf throws if the element is null (thanks to @charlescyt) 29 | 30 | ## 0.6.3 - 2024-03-16 31 | 32 | - Parses `debug`/`config` flags 33 | 34 | ## 0.6.2 - 2024-02-19 35 | 36 | - Fix null exception when using `TypeChecker.isSuperTypeOf` (thanks to @charlescyt) 37 | 38 | ## 0.6.1 - 2024-02-14 39 | 40 | - Exported `NodeLintRegistry` 41 | 42 | ## 0.6.0 - 2024-02-04 43 | 44 | - Bumped minimum Dart SDK to 3.0.0 45 | - goldens with diffs now include the priority, ID, selection and file path. 46 | - **breaking**: `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot`'s `source` 47 | parameter now takes a `Map? sources` instead of `String? source`. 48 | This enables goldens to handle fixes that emit to a different file. 49 | 50 | ## 0.5.14 - 2024-02-03 51 | 52 | - Improved formatting when specifying `source` on `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot` 53 | 54 | ## 0.5.13 - 2024-02-03 55 | 56 | - Improved formatting when specifying `source` on `encodePrioritizedSourceChanges`/`matcherNormalizedPrioritizedSourceChangeSnapshot` 57 | 58 | ## 0.5.12 - 2024-02-02 59 | 60 | - Added `encodePrioritizedSourceChanges`, to enable writing a `List` to a file 61 | - `matcherNormalizedPrioritizedSourceChangeSnapshot` now optionally 62 | takes a `String source`. This enables saving to the disk the expected 63 | result. 64 | 65 | ## 0.5.11 - 2024-01-27 66 | 67 | - `custom_lint` upgraded to `0.5.11` 68 | 69 | ## 0.5.10 - 2024-01-26 70 | 71 | - Fix a bug with `matcherNormalizedPrioritizedSourceChangeSnapshot` 72 | 73 | ## 0.5.9 - 2024-01-26 74 | 75 | - `matcherNormalizedPrioritizedSourceChangeSnapshot` now optionally allows specifying a `JsonEncoder`. 76 | 77 | ## 0.5.8 - 2024-01-09 78 | 79 | Added an optional `customPath` to the various `ChangeReporter` methods (thanks to @laurentschall) 80 | 81 | ## 0.5.7 - 2023-11-20 82 | 83 | - `custom_lint` upgraded to `0.5.7` 84 | 85 | ## 0.5.6 - 2023-10-30 86 | 87 | - `custom_lint` upgraded to `0.5.6` 88 | 89 | ## 0.5.5 - 2023-10-26 90 | 91 | - `custom_lint` upgraded to `0.5.5` 92 | 93 | ## 0.5.4 - 2023-10-20 94 | 95 | - `custom_lint` upgraded to `0.5.4` 96 | 97 | ## 0.5.3 - 2023-08-29 98 | 99 | - `custom_lint` upgraded to `0.5.3` 100 | 101 | ## 0.5.2 - 2023-08-16 102 | 103 | - Support both analyzer 5.12.0 and 6.0.0 at the same time. 104 | - Attempt at fixing the windows crash 105 | 106 | ## 0.5.1 - 2023-08-03 107 | 108 | Support analyzer v6 109 | 110 | ## 0.5.0 - 2023-06-21 111 | 112 | - `custom_lint` upgraded to `0.5.0` 113 | 114 | ## 0.4.0 - 2023-05-12 115 | 116 | - Added support for analyzer 5.12.0 117 | 118 | ## 0.3.4 - 2023-04-19 119 | 120 | - `custom_lint` upgraded to `0.3.4` 121 | 122 | ## 0.3.3 - 2023-04-06 123 | 124 | - Upgraded `analyzer` to `>=5.7.0 <5.11.0` 125 | - `LintRuleNodeRegistry` and other AstVisitor-like now are based off `GeneralizingAstVisitor` instead of `GeneralizingAstVisitor` 126 | - Exposes the Pubspec in CustomLintContext 127 | 128 | ## 0.3.2 - 2023-03-09 129 | 130 | - `custom_lint` upgraded to `0.3.2` 131 | 132 | ## 0.3.1 - 2023-03-09 133 | 134 | Update dependencies 135 | 136 | ## 0.3.0 - 2023-03-09 137 | 138 | - Update analyzer to >=5.7.0 <5.8.0 139 | 140 | ## 0.2.12 141 | 142 | Upgrade custom_lint 143 | 144 | ## 0.2.11 145 | 146 | Bump minimum Dart SDK to `sdk: ">=2.19.0 <3.0.0"` 147 | 148 | ## 0.2.10 149 | 150 | Update `fileToAnalyze` from `*.dart` to `**.dart` to match the `fileToAnalyze` fix in `custom_lint_builder` 151 | 152 | ## 0.2.9 153 | 154 | Fix `TypeChecker.fromPackage` not always return `true` when it should 155 | 156 | ## 0.2.8 157 | 158 | Fix exception thrown by `TypeChecker.isExactlyType` if `DartType.element` is `null`. 159 | 160 | ## 0.2.7 161 | 162 | Initial release 163 | -------------------------------------------------------------------------------- /packages/custom_lint_core/README.md: -------------------------------------------------------------------------------- 1 |

2 |

custom_lint_core

3 | An package exposing base classes for defining lint rules/fixes/assists. 4 |

5 | 6 |

7 | License 8 |

9 | 10 | ## About 11 | 12 | `custom_lint_core`, a variant of `custom_lint_builder` which exports lint-utilities without 13 | causing custom_lint to consider the dependent as a lint plugin. 14 | 15 | As opposed to `custom_lint_builder` , adding `custom_lint_core` as dependency will not flag 16 | a package as a "custom_lint plugin". 17 | 18 | See [custom_lint] for more informations 19 | 20 | --- 21 | 22 |

23 | 24 | 25 | 26 |

27 | Built and maintained by Invertase. 28 |

29 |

30 | 31 | [custom_lint]: https://github.com/invertase/dart_custom_lint 32 | -------------------------------------------------------------------------------- /packages/custom_lint_core/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | lint_visitor_generator: 5 | enabled: true 6 | generate_for: 7 | include: 8 | - "**/node_lint_visitor.dart" 9 | source_gen|combining_builder: 10 | options: 11 | ignore_for_file: 12 | - "type=lint" 13 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/custom_lint_core.dart: -------------------------------------------------------------------------------- 1 | export 'package:custom_lint_visitor/custom_lint_visitor.dart' 2 | hide LintRegistry, LinterVisitor, NodeLintRegistry; 3 | 4 | export 'src/assist.dart'; 5 | export 'src/change_reporter.dart' 6 | hide 7 | BatchChangeReporterBuilder, 8 | BatchChangeReporterImpl, 9 | ChangeBuilderImpl, 10 | ChangeReporterBuilder, 11 | ChangeReporterBuilderImpl, 12 | ChangeReporterImpl; 13 | export 'src/configs.dart'; 14 | export 'src/fixes.dart' hide FixArgs; 15 | export 'src/lint_codes.dart'; 16 | export 'src/lint_rule.dart'; 17 | export 'src/matcher.dart'; 18 | export 'src/package_utils.dart' hide FindProjectError; 19 | export 'src/plugin_base.dart' hide runPostRunCallbacks; 20 | export 'src/resolver.dart' hide CustomLintResolverImpl; 21 | export 'src/source_range_extensions.dart'; 22 | export 'src/type_checker.dart'; 23 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/assist.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' as io; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:analyzer/dart/analysis/utilities.dart'; 5 | import 'package:analyzer/source/source_range.dart'; 6 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 7 | import 'package:custom_lint_visitor/custom_lint_visitor.dart'; 8 | import 'package:meta/meta.dart'; 9 | import 'package:pubspec_parse/pubspec_parse.dart'; 10 | 11 | import 'change_reporter.dart'; 12 | import 'fixes.dart'; 13 | import 'lint_rule.dart'; 14 | import 'plugin_base.dart'; 15 | import 'resolver.dart'; 16 | 17 | /// A base class for assists. 18 | /// 19 | /// Assists are more typically known as "refactoring". They are changes 20 | /// triggered by the user, without an associated problem. As opposed to a [Fix], 21 | /// which represents a source change but is associated with an issue. 22 | /// 23 | /// For creating assists inside Dart files, see [DartAssist]. 24 | /// 25 | /// Suclassing [Assist] can be helpful if you wish to implement assists for 26 | /// non-Dart files (yaml, json, ...) 27 | /// 28 | /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/assists.md 29 | @immutable 30 | abstract class Assist { 31 | /// A list of glob patterns matching the files that [run] cares about. 32 | /// 33 | /// This can include Dart files, Yaml files, ... 34 | List get filesToAnalyze; 35 | 36 | /// Emits lints for a given file. 37 | /// 38 | /// [run] will only be invoked with files respecting [filesToAnalyze] 39 | Future startUp( 40 | CustomLintResolver resolver, 41 | CustomLintContext context, 42 | SourceRange target, 43 | ) async {} 44 | 45 | /// Emits lints for a given file. 46 | /// 47 | /// [run] will only be invoked with files respecting [filesToAnalyze] 48 | void run( 49 | CustomLintResolver resolver, 50 | ChangeReporter reporter, 51 | CustomLintContext context, 52 | SourceRange target, 53 | ); 54 | } 55 | 56 | /// A base class for creating assists inside Dart files. 57 | /// 58 | /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/assists.md#Defining-a-dart-assist 59 | @immutable 60 | abstract class DartAssist extends Assist { 61 | static final _stateKey = Object(); 62 | 63 | @override 64 | List get filesToAnalyze => const ['**.dart']; 65 | 66 | @override 67 | Future startUp( 68 | CustomLintResolver resolver, 69 | CustomLintContext context, 70 | SourceRange target, 71 | ) async { 72 | // Relying on shared state to execute all linters in a single AstVisitor 73 | if (context.sharedState.containsKey(_stateKey)) return; 74 | context.sharedState[_stateKey] = Object(); 75 | 76 | final unit = await resolver.getResolvedUnitResult(); 77 | 78 | context.addPostRunCallback(() { 79 | final linterVisitor = LinterVisitor(context.registry.nodeLintRegistry); 80 | 81 | unit.unit.accept(linterVisitor); 82 | }); 83 | } 84 | 85 | /// Runs this assist in test mode. 86 | /// 87 | /// The result will contain all the changes that would have been applied by [run]. 88 | /// 89 | /// The parameter [pubspec] can be used to simulate a pubspec file which will 90 | /// be passed to [CustomLintContext.pubspec]. 91 | /// By default, an empty pubspec with the name `test_project` will be used. 92 | @visibleForTesting 93 | Future> testRun( 94 | ResolvedUnitResult result, 95 | SourceRange target, { 96 | Pubspec? pubspec, 97 | }) async { 98 | final registry = LintRuleNodeRegistry( 99 | NodeLintRegistry(LintRegistry(), enableTiming: false), 100 | 'unknown', 101 | ); 102 | final postRunCallbacks = []; 103 | final context = CustomLintContext( 104 | registry, 105 | postRunCallbacks.add, 106 | {}, 107 | pubspec, 108 | ); 109 | final resolver = CustomLintResolverImpl( 110 | () => Future.value(result), 111 | lineInfo: result.lineInfo, 112 | path: result.path, 113 | source: result.libraryElement.source, 114 | ); 115 | final reporter = ChangeReporterImpl(result.session, resolver); 116 | 117 | await startUp( 118 | resolver, 119 | context, 120 | target, 121 | ); 122 | 123 | run(resolver, reporter, context, target); 124 | runPostRunCallbacks(postRunCallbacks); 125 | 126 | return reporter.complete(); 127 | } 128 | 129 | /// Analyze a Dart file and runs this assist in test mode. 130 | /// 131 | /// The result will contain all the changes that would have been applied by [run]. 132 | @visibleForTesting 133 | Future> testAnalyzeAndRun( 134 | io.File file, 135 | SourceRange target, 136 | ) async { 137 | final result = await resolveFile2(path: file.path); 138 | result as ResolvedUnitResult; 139 | return testRun(result, target); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/fixes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:analyzer/dart/analysis/utilities.dart'; 5 | import 'package:analyzer/error/error.dart' 6 | hide 7 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 8 | LintCode; 9 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 10 | import 'package:custom_lint_visitor/custom_lint_visitor.dart'; 11 | import 'package:meta/meta.dart'; 12 | import 'package:pubspec_parse/pubspec_parse.dart'; 13 | import 'package:uuid/uuid.dart'; 14 | 15 | import 'change_reporter.dart'; 16 | import 'lint_rule.dart'; 17 | import 'plugin_base.dart'; 18 | import 'resolver.dart'; 19 | import 'runnable.dart'; 20 | 21 | /// Args for [Fix]. 22 | @internal 23 | typedef FixArgs = ({ 24 | ChangeReporter reporter, 25 | AnalysisError analysisError, 26 | List others, 27 | }); 28 | 29 | const _uid = Uuid(); 30 | 31 | /// {@template custom_lint_builder.lint_rule} 32 | /// A base class for defining quick-fixes for a [LintRule] 33 | /// 34 | /// For creating assists inside Dart files, see [DartFix]. 35 | /// Subclassing [Fix] can be helpful if you wish to implement assists for 36 | /// non-Dart files (yaml, json, ...) 37 | /// 38 | /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/fixes.md 39 | /// {@endtemplate} 40 | @immutable 41 | abstract class Fix extends Runnable { 42 | /// A unique ID for a fix. Must be unique across all fixes of any package. 43 | /// 44 | /// This is used to know which fix triggered a change, for batch support. 45 | late final String id = _uid.v4(); 46 | 47 | /// A list of glob patterns matching the files that [run] cares about. 48 | /// 49 | /// This can include Dart files, Yaml files, ... 50 | List get filesToAnalyze; 51 | 52 | /// Emits lints for a given file. 53 | /// 54 | /// [run] will only be invoked with files respecting [filesToAnalyze] 55 | @override 56 | Future startUp( 57 | CustomLintResolver resolver, 58 | CustomLintContext context, 59 | ) async {} 60 | 61 | @internal 62 | @override 63 | void callRun( 64 | CustomLintResolver resolver, 65 | CustomLintContext context, 66 | FixArgs args, 67 | ) { 68 | run( 69 | resolver, 70 | args.reporter, 71 | context, 72 | args.analysisError, 73 | args.others, 74 | ); 75 | } 76 | 77 | /// Emits lints for a given file. 78 | /// 79 | /// [run] will only be invoked with files respecting [filesToAnalyze] 80 | /// Emits source changes for a given error. 81 | /// 82 | /// Optionally [others] can be specified with a list of similar errors within 83 | /// the same file. 84 | /// This can be used to provide an option for fixing multiple errors at once. 85 | void run( 86 | CustomLintResolver resolver, 87 | ChangeReporter reporter, 88 | CustomLintContext context, 89 | AnalysisError analysisError, 90 | List others, 91 | ); 92 | } 93 | 94 | /// A base class for defining quick-fixes inside Dart files. 95 | /// 96 | /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/fixes.md#Defining-dart-fix 97 | @immutable 98 | abstract class DartFix extends Fix { 99 | static final _stateKey = Object(); 100 | 101 | @override 102 | List get filesToAnalyze => const ['**.dart']; 103 | 104 | @override 105 | Future startUp( 106 | CustomLintResolver resolver, 107 | CustomLintContext context, 108 | ) async { 109 | // Relying on shared state to execute all linters in a single AstVisitor 110 | if (context.sharedState.containsKey(_stateKey)) return; 111 | context.sharedState[_stateKey] = Object(); 112 | 113 | final unit = await resolver.getResolvedUnitResult(); 114 | 115 | context.addPostRunCallback(() { 116 | final linterVisitor = LinterVisitor(context.registry.nodeLintRegistry); 117 | 118 | unit.unit.accept(linterVisitor); 119 | }); 120 | } 121 | 122 | /// Runs this fix in test mode. 123 | /// 124 | /// The result will contain all the changes that would have been applied by [run]. 125 | /// 126 | /// The parameter [pubspec] can be used to simulate a pubspec file which will 127 | /// be passed to [CustomLintContext.pubspec]. 128 | /// By default, an empty pubspec with the name `test_project` will be used. 129 | @visibleForTesting 130 | Future> testRun( 131 | ResolvedUnitResult result, 132 | AnalysisError analysisError, 133 | List others, { 134 | Pubspec? pubspec, 135 | }) async { 136 | final registry = LintRuleNodeRegistry( 137 | NodeLintRegistry(LintRegistry(), enableTiming: false), 138 | 'unknown', 139 | ); 140 | final postRunCallbacks = []; 141 | final context = CustomLintContext( 142 | registry, 143 | postRunCallbacks.add, 144 | {}, 145 | pubspec, 146 | ); 147 | final resolver = CustomLintResolverImpl( 148 | () => Future.value(result), 149 | lineInfo: result.lineInfo, 150 | path: result.path, 151 | source: result.libraryElement.source, 152 | ); 153 | final reporter = ChangeReporterImpl(result.session, resolver); 154 | 155 | await startUp(resolver, context); 156 | run(resolver, reporter, context, analysisError, others); 157 | runPostRunCallbacks(postRunCallbacks); 158 | 159 | return reporter.complete(); 160 | } 161 | 162 | /// Analyze a Dart file and runs this fix in test mode. 163 | /// 164 | /// The result will contain all the changes that would have been applied by [run]. 165 | @visibleForTesting 166 | Future> testAnalyzeAndRun( 167 | File file, 168 | AnalysisError analysisError, 169 | List others, 170 | ) async { 171 | final result = await resolveFile2(path: file.path); 172 | result as ResolvedUnitResult; 173 | return testRun(result, analysisError, others); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/lint_codes.dart: -------------------------------------------------------------------------------- 1 | // Forked from package:analyzer/src/dart/error/lint_codes.dart 2 | 3 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 4 | // for details. All rights reserved. Use of this source code is governed by a 5 | // BSD-style license that can be found in the LICENSE file. 6 | 7 | import 'package:analyzer/error/error.dart' 8 | hide 9 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 10 | LintCode; 11 | import 'package:meta/meta.dart'; 12 | 13 | import '../custom_lint_core.dart'; 14 | 15 | /// A class representing an [ErrorCode] for [LintRule]s. 16 | @immutable 17 | class LintCode extends ErrorCode { 18 | /// A class representing an [ErrorCode] for [LintRule]s. 19 | const LintCode({ 20 | required super.name, 21 | required super.problemMessage, 22 | super.correctionMessage, 23 | String? uniqueName, 24 | this.url, 25 | this.errorSeverity = ErrorSeverity.INFO, 26 | }) : super( 27 | uniqueName: uniqueName ?? name, 28 | ); 29 | 30 | @override 31 | ErrorType get type => ErrorType.LINT; 32 | 33 | @override 34 | final String? url; 35 | 36 | @override 37 | final ErrorSeverity errorSeverity; 38 | 39 | @override 40 | int get hashCode => uniqueName.hashCode; 41 | 42 | @override 43 | bool operator ==(Object other) { 44 | return other is LintCode && uniqueName == other.uniqueName; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/plugin_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'assist.dart'; 6 | import 'configs.dart'; 7 | import 'lint_rule.dart'; 8 | 9 | /// Runs a list of "postRun" callbacks. 10 | /// 11 | /// Errors are caught to ensure all callbacks are executed. 12 | @internal 13 | void runPostRunCallbacks(List postRunCallbacks) { 14 | for (final postCallback in postRunCallbacks) { 15 | try { 16 | postCallback(); 17 | } catch (err, stack) { 18 | Zone.current.handleUncaughtError(err, stack); 19 | // All postCallbacks should execute even if one throw 20 | } 21 | } 22 | } 23 | 24 | /// A base class for custom analyzer plugins 25 | /// 26 | /// If a print is emitted or an exception is uncaught, 27 | abstract class PluginBase { 28 | /// Returns a list of warning/infos/errors for a Dart file. 29 | List getLintRules(CustomLintConfigs configs); 30 | 31 | /// Obtains the list of assists created by this plugin. 32 | List getAssists() => const []; 33 | } 34 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/pragmas.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Alias for vm:prefer-inline 4 | @internal 5 | const preferInline = pragma('vm:prefer-inline'); 6 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/resolver.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:analyzer/dart/analysis/session.dart'; 3 | import 'package:analyzer/source/line_info.dart'; 4 | import 'package:analyzer/source/source.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | /// A class used to interact with files and possibly emit lints of out it. 8 | /// 9 | /// The file analyzed might not be a Dart file. 10 | abstract class CustomLintResolver { 11 | /// The file path that is being analyzed. 12 | String get path; 13 | 14 | /// The content of the file that is being analyzed. 15 | Source get source; 16 | 17 | /// Line/column/offset metadata about [source]. 18 | LineInfo get lineInfo; 19 | 20 | /// Obtains a decoded representation of a Dart file. 21 | /// 22 | /// It is safe to invoke this method multiple times, as the future is cached. 23 | /// 24 | /// May throw an [InconsistentAnalysisException] 25 | Future getResolvedUnitResult(); 26 | } 27 | 28 | /// The implementation of [CustomLintResolver] 29 | @internal 30 | class CustomLintResolverImpl extends CustomLintResolver { 31 | /// The implementation of [CustomLintResolver] 32 | CustomLintResolverImpl( 33 | this._getResolvedUnitResult, { 34 | required this.lineInfo, 35 | required this.source, 36 | required this.path, 37 | }); 38 | 39 | @override 40 | final LineInfo lineInfo; 41 | 42 | @override 43 | final Source source; 44 | 45 | @override 46 | final String path; 47 | 48 | final Future Function() _getResolvedUnitResult; 49 | 50 | Future? _getResolvedUnitResultFuture; 51 | 52 | @override 53 | Future getResolvedUnitResult() { 54 | return _getResolvedUnitResultFuture ??= _getResolvedUnitResult(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/runnable.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'lint_rule.dart'; 4 | import 'resolver.dart'; 5 | 6 | /// A base-class for runnable objects. 7 | abstract class Runnable { 8 | /// Initializes the runnable object. 9 | Future startUp( 10 | CustomLintResolver resolver, 11 | CustomLintContext context, 12 | ); 13 | 14 | /// Runs the runnable object. 15 | @internal 16 | void callRun( 17 | CustomLintResolver resolver, 18 | CustomLintContext context, 19 | RunArgs args, 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/custom_lint_core/lib/src/source_range_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/ast/syntactic_entity.dart'; 2 | import 'package:analyzer/error/error.dart' 3 | hide 4 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 5 | LintCode; 6 | import 'package:analyzer/source/source_range.dart'; 7 | 8 | /// Adds [sourceRange] 9 | extension SyntacticEntitySourceRange on SyntacticEntity { 10 | /// A [SourceRange] based on [offset] + [length] 11 | SourceRange get sourceRange => SourceRange(offset, length); 12 | } 13 | 14 | /// Adds [sourceRange] 15 | extension AnalysisErrorSourceRange on AnalysisError { 16 | /// A [SourceRange] based on [offset] + [length] 17 | SourceRange get sourceRange => SourceRange(offset, length); 18 | } 19 | -------------------------------------------------------------------------------- /packages/custom_lint_core/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_core 2 | version: 0.7.5 3 | description: A package to help writing custom linters 4 | repository: https://github.com/invertase/dart_custom_lint 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | analyzer: ^7.0.0 11 | analyzer_plugin: ^0.13.0 12 | collection: ^1.16.0 13 | custom_lint_visitor: ^1.0.0 14 | glob: ^2.1.2 15 | matcher: ^0.12.0 16 | meta: ^1.7.0 17 | package_config: ^2.1.0 18 | path: ^1.8.0 19 | pubspec_parse: ^1.2.2 20 | source_span: ^1.8.0 21 | uuid: ^4.5.1 22 | yaml: ^3.1.1 23 | 24 | dev_dependencies: 25 | build_runner: ^2.3.3 26 | lint_visitor_generator: 27 | path: ../lint_visitor_generator 28 | test: ^1.22.2 29 | -------------------------------------------------------------------------------- /packages/custom_lint_core/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: lint_visitor_generator,custom_lint_visitor 2 | dependency_overrides: 3 | custom_lint: 4 | path: ../custom_lint 5 | lint_visitor_generator: 6 | path: ../lint_visitor_generator 7 | -------------------------------------------------------------------------------- /packages/custom_lint_core/test/fix_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:analyzer/dart/analysis/utilities.dart'; 3 | import 'package:analyzer/error/error.dart' 4 | hide 5 | // ignore: undefined_hidden_name, Needed to support lower analyzer versions 6 | LintCode; 7 | import 'package:custom_lint_core/src/change_reporter.dart'; 8 | import 'package:custom_lint_core/src/fixes.dart'; 9 | import 'package:custom_lint_core/src/lint_rule.dart'; 10 | import 'package:custom_lint_core/src/matcher.dart'; 11 | import 'package:custom_lint_core/src/resolver.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | import 'assist_test.dart'; 15 | import 'lint_rule_test.dart'; 16 | 17 | void main() { 18 | test('Fix.testRun', () async { 19 | final fix = MyFix('MyAssist'); 20 | final fix2 = MyFix('Another'); 21 | 22 | const fileSource = ''' 23 | void main() { 24 | print('Hello world'); 25 | } 26 | '''; 27 | final file = writeToTemporaryFile(fileSource); 28 | final result = await resolveFile2(path: file.path); 29 | result as ResolvedUnitResult; 30 | 31 | final errors = await const MyLintRule().testRun(result); 32 | 33 | final changes = fix.testRun(result, errors.single, errors); 34 | final changes2 = fix2.testRun(result, errors.single, errors); 35 | 36 | expect( 37 | await changes, 38 | matcherNormalizedPrioritizedSourceChangeSnapshot( 39 | 'snapshot.diff', 40 | sources: {'**/*': fileSource}, 41 | relativePath: file.parent.path, 42 | ), 43 | ); 44 | expect( 45 | await changes, 46 | isNot( 47 | matcherNormalizedPrioritizedSourceChangeSnapshot( 48 | 'snapshot2.diff', 49 | sources: {'**/*': fileSource}, 50 | relativePath: file.parent.path, 51 | ), 52 | ), 53 | ); 54 | 55 | expect( 56 | await changes2, 57 | isNot( 58 | matcherNormalizedPrioritizedSourceChangeSnapshot( 59 | 'snapshot.diff', 60 | sources: {'**/*': fileSource}, 61 | relativePath: file.parent.path, 62 | ), 63 | ), 64 | ); 65 | expect( 66 | await changes2, 67 | matcherNormalizedPrioritizedSourceChangeSnapshot( 68 | 'snapshot2.diff', 69 | sources: {'**/*': fileSource}, 70 | relativePath: file.parent.path, 71 | ), 72 | ); 73 | }); 74 | 75 | test('Fix.testAnalyzeRun', () async { 76 | final fix = MyFix('MyAssist'); 77 | 78 | const fileSource = ''' 79 | void main() { 80 | print('Hello world'); 81 | } 82 | '''; 83 | final file = writeToTemporaryFile(fileSource); 84 | final errors = await const MyLintRule().testAnalyzeAndRun(file); 85 | 86 | final changes = fix.testAnalyzeAndRun(file, errors.single, errors); 87 | 88 | expect( 89 | await changes, 90 | matcherNormalizedPrioritizedSourceChangeSnapshot( 91 | 'snapshot.diff', 92 | sources: {'**/*': fileSource}, 93 | relativePath: file.parent.path, 94 | ), 95 | ); 96 | expect( 97 | await changes, 98 | isNot( 99 | matcherNormalizedPrioritizedSourceChangeSnapshot( 100 | 'snapshot2.diff', 101 | sources: {'**/*': fileSource}, 102 | relativePath: file.parent.path, 103 | ), 104 | ), 105 | ); 106 | }); 107 | } 108 | 109 | class MyFix extends DartFix { 110 | MyFix(this.name); 111 | 112 | final String name; 113 | 114 | @override 115 | void run( 116 | CustomLintResolver resolver, 117 | ChangeReporter reporter, 118 | CustomLintContext context, 119 | AnalysisError analysisError, 120 | List others, 121 | ) { 122 | context.registry.addMethodInvocation((node) { 123 | final changeBuilder = reporter.createChangeBuilder( 124 | message: name, 125 | priority: 1, 126 | ); 127 | 128 | changeBuilder.addGenericFileEdit((builder) { 129 | builder.addSimpleInsertion(node.offset, 'Hello'); 130 | }); 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/custom_lint_core/test/lint_rule_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:analyzer/dart/analysis/utilities.dart'; 5 | import 'package:analyzer/error/listener.dart'; 6 | import 'package:custom_lint_core/custom_lint_core.dart'; 7 | 8 | import 'package:test/test.dart'; 9 | 10 | import 'assist_test.dart'; 11 | import 'configs_test.dart'; 12 | 13 | class TestLintRule extends LintRule { 14 | const TestLintRule({required this.enabledByDefault}) 15 | : super( 16 | code: const LintCode( 17 | name: 'test_lint', 18 | problemMessage: 'Test lint', 19 | ), 20 | ); 21 | 22 | @override 23 | List get filesToAnalyze => ['*']; 24 | 25 | @override 26 | final bool enabledByDefault; 27 | 28 | @override 29 | void run( 30 | CustomLintResolver resolver, 31 | ErrorReporter reporter, 32 | CustomLintContext context, 33 | ) {} 34 | } 35 | 36 | class MyLintRule extends DartLintRule { 37 | const MyLintRule() 38 | : super( 39 | code: const LintCode( 40 | name: 'my_lint_code', 41 | problemMessage: 'message', 42 | ), 43 | ); 44 | 45 | @override 46 | void run( 47 | CustomLintResolver resolver, 48 | ErrorReporter reporter, 49 | CustomLintContext context, 50 | ) { 51 | context.registry.addMethodInvocation((node) { 52 | reporter.atNode(node.methodName, code); 53 | }); 54 | } 55 | } 56 | 57 | void main() async { 58 | const onByDefault = TestLintRule(enabledByDefault: true); 59 | const offByDefault = TestLintRule(enabledByDefault: false); 60 | final packageConfig = await parsePackageConfig(Directory.current); 61 | 62 | test('LintRule.testRun', () async { 63 | const assist = MyLintRule(); 64 | 65 | final file = writeToTemporaryFile(''' 66 | void main() { 67 | print('Hello world'); 68 | } 69 | '''); 70 | final result = await resolveFile2(path: file.path); 71 | result as ResolvedUnitResult; 72 | 73 | final analysisErrors = await assist.testRun(result); 74 | 75 | expect(analysisErrors, hasLength(1)); 76 | 77 | expect(analysisErrors.first.errorCode.name, 'my_lint_code'); 78 | expect(analysisErrors.first.message, 'message'); 79 | expect(analysisErrors.first.offset, 16); 80 | expect(analysisErrors.first.length, 'print'.length); 81 | }); 82 | 83 | test('LintRule.testAnalyzeAndRun', () async { 84 | const assist = MyLintRule(); 85 | 86 | final file = writeToTemporaryFile(''' 87 | void main() { 88 | print('Hello world'); 89 | } 90 | '''); 91 | 92 | final analysisErrors = await assist.testAnalyzeAndRun(file); 93 | 94 | expect(analysisErrors, hasLength(1)); 95 | 96 | expect(analysisErrors.first.errorCode.name, 'my_lint_code'); 97 | expect(analysisErrors.first.message, 'message'); 98 | expect(analysisErrors.first.offset, 16); 99 | expect(analysisErrors.first.length, 'print'.length); 100 | }); 101 | 102 | group('LintRule.isEnabled', () { 103 | test('defaults to checking "enabedByDefault"', () { 104 | expect(onByDefault.isEnabled(CustomLintConfigs.empty), true); 105 | expect(offByDefault.isEnabled(CustomLintConfigs.empty), false); 106 | }); 107 | 108 | test('always enabled if on in the config files', () { 109 | final analysisOptionFile = createAnalysisOptions(''' 110 | custom_lint: 111 | rules: 112 | - test_lint 113 | '''); 114 | final configs = 115 | CustomLintConfigs.parse(analysisOptionFile, packageConfig); 116 | 117 | expect(onByDefault.isEnabled(configs), true); 118 | expect(offByDefault.isEnabled(configs), true); 119 | }); 120 | 121 | test('always disabled if off in the config files', () { 122 | final analysisOptionFile = createAnalysisOptions(''' 123 | custom_lint: 124 | rules: 125 | - test_lint: false 126 | '''); 127 | final configs = 128 | CustomLintConfigs.parse(analysisOptionFile, packageConfig); 129 | 130 | expect(onByDefault.isEnabled(configs), false); 131 | expect(offByDefault.isEnabled(configs), false); 132 | }); 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /packages/custom_lint_core/test/snapshot.diff: -------------------------------------------------------------------------------- 1 | Message: `MyAssist` 2 | Priority: 1 3 | Diff for file `file.dart:2`: 4 | ``` 5 | void main() { 6 | - print('Hello world'); 7 | + Helloprint('Hello world'); 8 | } 9 | ``` 10 | --- 11 | -------------------------------------------------------------------------------- /packages/custom_lint_core/test/snapshot2.diff: -------------------------------------------------------------------------------- 1 | Message: `Another` 2 | Priority: 1 3 | Diff for file `file.dart:2`: 4 | ``` 5 | void main() { 6 | - print('Hello world'); 7 | + Helloprint('Hello world'); 8 | } 9 | ``` 10 | --- 11 | -------------------------------------------------------------------------------- /packages/custom_lint_core/test/type_checker_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:analyzer/dart/analysis/utilities.dart'; 5 | import 'package:analyzer/dart/ast/ast.dart'; 6 | import 'package:analyzer/dart/ast/visitor.dart'; 7 | import 'package:custom_lint_core/custom_lint_core.dart'; 8 | import 'package:path/path.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'assist_test.dart'; 12 | 13 | void main() { 14 | test('Can call isExactlyType in DartTypes with no element', () async { 15 | final file = writeToTemporaryFile(''' 16 | void main() { 17 | void Function()? fn; 18 | fn?.call(); 19 | } 20 | '''); 21 | 22 | final unit = await resolveFile2(path: file.path); 23 | unit as ResolvedUnitResult; 24 | 25 | const checker = TypeChecker.fromName('foo'); 26 | 27 | unit.unit.accept( 28 | _MethodInvocationVisitor((node) { 29 | expect( 30 | checker.isExactlyType(node.target!.staticType!), 31 | isFalse, 32 | ); 33 | }), 34 | ); 35 | }); 36 | 37 | test('Can call isSuperTypeOf in DartTypes with no element', () async { 38 | final file = writeToTemporaryFile(r''' 39 | void fn((int, String) record) { 40 | final first = record.$1; 41 | } 42 | '''); 43 | 44 | final unit = await resolveFile2(path: file.path); 45 | unit as ResolvedUnitResult; 46 | 47 | const checker = TypeChecker.fromName('record'); 48 | 49 | late final PropertyAccess propertyAccessNode; 50 | unit.unit.accept( 51 | _PropertyAccessVisitor((node) { 52 | propertyAccessNode = node; 53 | }), 54 | ); 55 | 56 | expect(propertyAccessNode.realTarget.staticType!.element, isNull); 57 | expect( 58 | checker.isExactlyType(propertyAccessNode.realTarget.staticType!), 59 | isFalse, 60 | ); 61 | }); 62 | 63 | group('TypeChecker.fromPackage', () { 64 | test('matches a type from a package', () async { 65 | final tempDir = Directory.systemTemp.createTempSync(); 66 | addTearDown(() => tempDir.deleteSync(recursive: true)); 67 | 68 | final file = File(join(tempDir.path, 'lib', 'main.dart')) 69 | ..createSync(recursive: true) 70 | ..writeAsStringSync(''' 71 | class Foo {} 72 | int a; 73 | '''); 74 | 75 | final pubspec = File(join(tempDir.path, 'pubspec.yaml')); 76 | pubspec.writeAsStringSync(''' 77 | name: some_package 78 | version: 0.2.8 79 | description: A package to help writing custom linters 80 | repository: https://github.com/invertase/dart_custom_lint 81 | environment: 82 | sdk: ">=3.0.0 <4.0.0" 83 | '''); 84 | 85 | await Process.run( 86 | 'dart', 87 | ['pub', 'get', '--offline'], 88 | workingDirectory: tempDir.path, 89 | ); 90 | 91 | final unit = await resolveFile2(path: file.path); 92 | unit as ResolvedUnitResult; 93 | 94 | const checker = TypeChecker.fromPackage('some_package'); 95 | const checker2 = TypeChecker.fromPackage('some_package2'); 96 | 97 | expect( 98 | checker.isExactlyType( 99 | (unit.unit.declarations.first as ClassDeclaration) 100 | .declaredElement! 101 | .thisType, 102 | ), 103 | true, 104 | ); 105 | expect( 106 | checker2.isExactlyType( 107 | (unit.unit.declarations.first as ClassDeclaration) 108 | .declaredElement! 109 | .thisType, 110 | ), 111 | false, 112 | ); 113 | 114 | expect( 115 | checker.isExactlyType( 116 | (unit.unit.declarations[1] as TopLevelVariableDeclaration) 117 | .variables 118 | .type! 119 | .type!, 120 | ), 121 | false, 122 | ); 123 | }); 124 | 125 | test('matches a type from a built-in dart: package', () async { 126 | final file = writeToTemporaryFile(''' 127 | import 'dart:io'; 128 | 129 | int a; 130 | File? x; 131 | '''); 132 | 133 | final unit = await resolveFile2(path: file.path); 134 | unit as ResolvedUnitResult; 135 | 136 | const checker = TypeChecker.fromPackage('dart:core'); 137 | const checker2 = TypeChecker.fromPackage('dart:io'); 138 | const checker3 = TypeChecker.fromPackage('some_package'); 139 | 140 | expect( 141 | checker.isExactlyType( 142 | (unit.unit.declarations.first as TopLevelVariableDeclaration) 143 | .variables 144 | .type! 145 | .type!, 146 | ), 147 | true, 148 | ); 149 | 150 | expect( 151 | checker.isExactlyType( 152 | (unit.unit.declarations[1] as TopLevelVariableDeclaration) 153 | .variables 154 | .type! 155 | .type!, 156 | ), 157 | false, 158 | ); 159 | 160 | expect( 161 | checker2.isExactlyType( 162 | (unit.unit.declarations[1] as TopLevelVariableDeclaration) 163 | .variables 164 | .type! 165 | .type!, 166 | ), 167 | true, 168 | ); 169 | 170 | expect( 171 | checker3.isExactlyType( 172 | (unit.unit.declarations.first as TopLevelVariableDeclaration) 173 | .variables 174 | .type! 175 | .type!, 176 | ), 177 | false, 178 | ); 179 | }); 180 | }); 181 | } 182 | 183 | class _MethodInvocationVisitor extends RecursiveAstVisitor { 184 | _MethodInvocationVisitor(this.onMethodInvocation); 185 | 186 | final void Function(MethodInvocation node) onMethodInvocation; 187 | 188 | @override 189 | void visitMethodInvocation(MethodInvocation node) { 190 | onMethodInvocation(node); 191 | super.visitMethodInvocation(node); 192 | } 193 | } 194 | 195 | class _PropertyAccessVisitor extends RecursiveAstVisitor { 196 | _PropertyAccessVisitor(this.onPropertyAccess); 197 | 198 | final void Function(PropertyAccess node) onPropertyAccess; 199 | 200 | @override 201 | void visitPropertyAccess(PropertyAccess node) { 202 | onPropertyAccess(node); 203 | super.visitPropertyAccess(node); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.0+ (7.4.5) 2 | 3 | Added support for analyzer 7.4.5 4 | 5 | ## 1.0.0+ (6.7.0/6.11.0/7.3.0) 6 | 7 | Initial release 8 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/README.md: -------------------------------------------------------------------------------- 1 |

2 |

custom_lint_core

3 | An package exposing base classes for defining lint rules/fixes/assists. 4 |

5 | 6 |

7 | License 8 |

9 | 10 | ## About 11 | 12 | `custom_lint_visitor` is a dependency of `custom_lint`, for the sake of supporting 13 | multiple Analyzer versions without causing too many breaking changes. 14 | 15 | It exposes various ways to traverse the tree of `AstNode`s using callbacks. 16 | 17 | ## Versioning 18 | 19 | One version of `custom_lint_visitor` is released for every `analyzer` version. 20 | 21 | The version `1.0.0+6.7.0` means "Version 1.0.0 of custom_lint_visitor, for analyzer's 6.7.0 version". 22 | 23 | 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` 24 | 25 | Depending on `custom_lint_visitor: ^1.0.0` will therefore support 26 | any compatible Analyzer version. 27 | To require a specific analyzer version, specify `analyzer: ` explicitly. -------------------------------------------------------------------------------- /packages/custom_lint_visitor/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | lint_visitor_generator: 5 | enabled: true 6 | generate_for: 7 | include: 8 | - "**/node_lint_visitor.dart" 9 | source_gen|combining_builder: 10 | options: 11 | ignore_for_file: 12 | - "type=lint" 13 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/lib/custom_lint_visitor.dart: -------------------------------------------------------------------------------- 1 | export 'src/node_lint_visitor.dart'; 2 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/lib/src/node_lint_visitor.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | 8 | import 'package:analyzer/dart/ast/ast.dart'; 9 | import 'package:analyzer/dart/ast/visitor.dart'; 10 | import 'pragmas.dart'; 11 | 12 | part 'node_lint_visitor.g.dart'; 13 | 14 | /// Manages lint timing. 15 | class LintRegistry { 16 | /// Dictionary mapping lints (by name) to timers. 17 | final Map timers = HashMap(); 18 | 19 | /// Get a timer associated with the given lint rule (or create one if none 20 | /// exists). 21 | Stopwatch getTimer(String name) { 22 | return timers.putIfAbsent(name, Stopwatch.new); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/lib/src/pragmas.dart: -------------------------------------------------------------------------------- 1 | /// Alias for vm:prefer-inline 2 | const preferInline = pragma('vm:prefer-inline'); 3 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_lint_visitor 2 | version: 1.0.0+7.4.5 3 | description: A package that exports visitors for CustomLint. 4 | repository: https://github.com/invertase/dart_custom_lint 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | analyzer: 7.4.5 11 | 12 | dev_dependencies: 13 | build_runner: ^2.3.3 14 | lint_visitor_generator: 15 | path: ../lint_visitor_generator 16 | test: ^1.22.2 17 | -------------------------------------------------------------------------------- /packages/custom_lint_visitor/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: lint_visitor_generator 2 | dependency_overrides: 3 | lint_visitor_generator: 4 | path: ../lint_visitor_generator 5 | -------------------------------------------------------------------------------- /packages/lint_visitor_generator/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Remi Rousselet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/lint_visitor_generator/README.md: -------------------------------------------------------------------------------- 1 | Internal tooling to generate `custom_lint_core/lib/src/node_lint_visitor.dart` -------------------------------------------------------------------------------- /packages/lint_visitor_generator/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | lint_visitor_generator: 5 | enabled: true 6 | generate_for: 7 | include: 8 | - "**/node_lint_visitor.dart" 9 | source_gen|combining_builder: 10 | options: 11 | ignore_for_file: 12 | - "type=lint" 13 | 14 | builders: 15 | lint_visitor_generator: 16 | import: "package:lint_visitor_generator/builder.dart" 17 | builder_factories: ["lintVisitorGenerator"] 18 | build_extensions: { ".dart": [".lint_visitor_generator.g.part"] } 19 | auto_apply: dependents 20 | build_to: cache 21 | applies_builders: ["source_gen|combining_builder"] -------------------------------------------------------------------------------- /packages/lint_visitor_generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: lint_visitor_generator 2 | publish_to: none 3 | 4 | environment: 5 | sdk: ">=3.0.0 <4.0.0" 6 | 7 | dependencies: 8 | analyzer: ^7.4.5 9 | build: ^2.3.1 10 | build_config: ^1.1.0 11 | collection: ^1.17.1 12 | meta: ^1.7.0 13 | source_gen: ^2.0.0 14 | 15 | dev_dependencies: 16 | build_runner: ^2.2.0 17 | build_test: ^2.1.5 18 | source_gen_test: ^1.0.4 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: melos_root 2 | publish_to: none 3 | 4 | environment: 5 | sdk: ">=3.0.0 <4.0.0" 6 | dev_dependencies: 7 | melos: ^3.0.0 8 | -------------------------------------------------------------------------------- /resources/lint_showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/resources/lint_showcase.png -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /website/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /website/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Minimal 2 | 3 | ``` 4 | npm create astro@latest -- --template minimal 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/minimal) 9 | 10 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 11 | 12 | ## 🚀 Project Structure 13 | 14 | Inside of your Astro project, you'll see the following folders and files: 15 | 16 | ``` 17 | / 18 | ├── public/ 19 | ├── src/ 20 | │ └── pages/ 21 | │ └── index.astro 22 | └── package.json 23 | ``` 24 | 25 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 26 | 27 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 28 | 29 | Any static assets, like images, can be placed in the `public/` directory. 30 | 31 | ## 🧞 Commands 32 | 33 | All commands are run from the root of the project, from a terminal: 34 | 35 | | Command | Action | 36 | | :--------------------- | :----------------------------------------------- | 37 | | `npm install` | Installs dependencies | 38 | | `npm run dev` | Starts local dev server at `localhost:3000` | 39 | | `npm run build` | Build your production site to `./dist/` | 40 | | `npm run preview` | Preview your build locally, before deploying | 41 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 42 | | `npm run astro --help` | Get help using the Astro CLI | 43 | 44 | ## 👀 Want to learn more? 45 | 46 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 47 | -------------------------------------------------------------------------------- /website/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | // https://astro.build/config 4 | import tailwind from "@astrojs/tailwind"; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | integrations: [tailwind()] 9 | }); -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/minimal", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/tailwind": "^3.0.0", 15 | "astro": "^2.0.1", 16 | "tailwindcss": "^3.2.4" 17 | }, 18 | "devDependencies": { 19 | "prettier": "2.8.3" 20 | } 21 | } -------------------------------------------------------------------------------- /website/public/favicons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /website/public/images/bg_header.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /website/public/images/bg_header_corner.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /website/public/images/bg_header_mobile.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /website/public/images/bg_plugins_shadow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/public/images/bg_shadow_l_b.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/public/images/bg_shadow_l_t.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/public/images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/public/images/cover.jpg -------------------------------------------------------------------------------- /website/public/images/lint_showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/public/images/lint_showcase.png -------------------------------------------------------------------------------- /website/public/images/particles_wave.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/public/images/particles_wave.webp -------------------------------------------------------------------------------- /website/public/images/vs_code_shadow_dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/public/images/vscode_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/public/images/vscode_example.jpg -------------------------------------------------------------------------------- /website/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import IconExternalLink from '@components/icons/IconExternalLink.astro'; 3 | 4 | interface Props { 5 | name: string; 6 | description: string; 7 | link: string; 8 | class?: string; 9 | } 10 | 11 | const { name, description, link, class: classStr } = Astro.props as Props; 12 | --- 13 | 14 | 42 | -------------------------------------------------------------------------------- /website/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import IconMoon from '@components/icons/IconMoon.astro'; 3 | import IconSun from '@components/icons/IconSun.astro'; 4 | import Logo from '@components/Logo.astro'; 5 | import nav from 'src/nav'; 6 | --- 7 | 8 | 56 | 69 | -------------------------------------------------------------------------------- /website/src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from '@components/Logo.astro'; 3 | import PluginCodeExample from './PluginCodeExample.astro'; 4 | import nav from 'src/nav'; 5 | import Toggle from '@components/Toggle.astro'; 6 | import MobileMenu from '@components/MobileMenu.astro'; 7 | --- 8 | 9 | 10 | 11 |
14 |
17 |
18 |
19 | 25 | 37 | 38 |
39 |
42 |
43 |

44 | Build powerful Custom Lint rules for any dart & flutter package 47 |

48 |

49 | Zero configuration, zero set up, simply start building! 50 |

51 | 55 | Go to Docs 56 | 57 |
58 |
59 |
62 | 63 | showcase 64 | 65 |
66 | 69 |
70 |
71 |
72 |
73 | 76 |
77 | -------------------------------------------------------------------------------- /website/src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | mainColor?: string; 4 | secondColor?: string; 5 | class?: string; 6 | } 7 | 8 | const { mainColor = '#333333', secondColor = '#2E2174', class: classStr } = Astro.props as Props; 9 | --- 10 | 11 | 19 | 28 | 29 | 30 | 31 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 51 | 52 | 54 | 55 | 58 | 61 | 64 | 65 | 66 | 72 | 73 | 74 | 75 | 84 | 85 | 90 | 91 | 92 | 93 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /website/src/components/MobileMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import nav from 'src/nav'; 3 | --- 4 | 5 |
13 |
    14 | { 15 | nav.map(({ title, href }) => ( 16 |
  • 17 | 18 | {title} 19 | 20 |
  • 21 | )) 22 | } 23 |
24 |
25 |
33 |
34 | 82 | -------------------------------------------------------------------------------- /website/src/components/PluginCodeExample.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from 'astro/components'; 3 | 4 | interface Props { 5 | class?: string; 6 | } 7 | 8 | const { class: classStr = '' } = Astro.props as Props; 9 | --- 10 | 11 |
12 |
15 |
18 |
21 | {'< >'} 22 | lib/my_package_name.dart 23 | X 24 |
25 | 26 |
27 | # 28 | pubspec.yaml 29 |
30 |
31 |
32 |
33 | _ExampleLinter(); 37 | class _ExampleLinter extends PluginBase { 38 | @override 39 | List getLintRules(CustomLintConfigs configs) => [ 40 | MyCustomLintCode(), 41 | ]; 42 | } 43 | class MyCustomLintCode extends DartLintRule { 44 | MyCustomLintCode() : super(code: _code); 45 | static const _code = LintCode( 46 | name: 'my_custom_lint_code', 47 | problemMessage: 'This is the description of our custom lint', 48 | ); 49 | @override 50 | void run( 51 | CustomLintResolver resolver, 52 | ErrorReporter reporter, 53 | CustomLintContext context, 54 | ) { 55 | context.registry.addVariableDeclaration((node) { 56 | reporter.atNode(node, code); 57 | }); 58 | } 59 | }`} 60 | /> 61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /website/src/components/Toggle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import IconChevronUp from '@components/icons/IconChevronUp.astro'; 3 | import IconHamburger from '@components/icons/IconHamburger.astro'; 4 | 5 | interface Props { 6 | class?: string; 7 | } 8 | 9 | const { class: classStr = '' } = Astro.props as Props; 10 | --- 11 | 12 | 24 | -------------------------------------------------------------------------------- /website/src/components/VsCodeBanner.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 27 |
28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /website/src/components/icons/IconChevronUp.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /website/src/components/icons/IconExternalLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 11 | 12 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /website/src/components/icons/IconHamburger.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 11 | 12 | 15 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /website/src/components/icons/IconMoon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /website/src/components/icons/IconSun.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 11 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 35 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /website/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /website/src/fonts/ITCAvantGardePro-Bk.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/src/fonts/ITCAvantGardePro-Bk.woff -------------------------------------------------------------------------------- /website/src/fonts/ITCAvantGardePro-Demi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/src/fonts/ITCAvantGardePro-Demi.woff -------------------------------------------------------------------------------- /website/src/fonts/ITCAvantGardePro-Md.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/src/fonts/ITCAvantGardePro-Md.woff -------------------------------------------------------------------------------- /website/src/fonts/ITCAvantGardeRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/invertase/dart_custom_lint/cea10792b219e8879ba645d636da045b1ff1eb46/website/src/fonts/ITCAvantGardeRegular.woff -------------------------------------------------------------------------------- /website/src/layouts/Root.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/fonts.css'; 3 | import '../styles/index.css'; 4 | 5 | export interface Props { 6 | // Sets the tag of the page 7 | title?: string; 8 | // Sets the <meta name="description" content="..."> tag of the page, and any social tags (e.g. og:description) 9 | description?: string; 10 | // Adds a social preview image to the page (e.g. twitter:image) 11 | image?: string; 12 | // Adds the twitter:creator meta tag to the page. 13 | author?: string; 14 | } 15 | 16 | const { 17 | title = 'Dart Custom Lint', 18 | description = 'Build powerful Custom Lint rules for any dart & flutter package', 19 | image = '/images/cover.jpg', 20 | author = 'Invertase', 21 | } = Astro.props; 22 | --- 23 | 24 | <html> 25 | <head> 26 | <meta charset="utf-8" /> 27 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 28 | 29 | <link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" /> 30 | <link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" /> 31 | <link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" /> 32 | <link rel="manifest" href="/favicons/site.webmanifest" /> 33 | <link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#333333" /> 34 | <link rel="shortcut icon" href="/favicons/favicon.ico" /> 35 | <meta name="msapplication-TileColor" content="#333333" /> 36 | <meta name="msapplication-config" content="/favicons/browserconfig.xml" /> 37 | <meta name="theme-color" content="#333333" /> 38 | <title>{title} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 66 | 67 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /website/src/nav.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | href: 'https://github.com/invertase/dart_custom_lint', 4 | title: 'Docs', 5 | }, 6 | { 7 | href: 'https://github.com/invertase/dart_custom_lint', 8 | title: 'Github', 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /website/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Root from '@layouts/Root.astro'; 3 | import Header from '@components/Header.astro'; 4 | import Footer from '@components/Footer.astro'; 5 | import Card from '@components/Card.astro'; 6 | // import VsCodeBanner from '@components/VsCodeBanner.astro'; 7 | 8 | const cards = [ 9 | { 10 | name: 'Riverpod Lints', 11 | description: 12 | 'a developper tool for users of Riverpod, designed to help stop common issue and simplify repetetive tasks.', 13 | link: 'https://pub.dev/packages/riverpod_lint', 14 | }, 15 | { 16 | name: 'Supernova Lints', 17 | description: 'Provides custom lint rules broadly used in supernova.io.', 18 | link: 'https://pub.dev/packages/supernova_lints', 19 | }, 20 | { 21 | name: 'Equatable Lints', 22 | description: 'This is a set of rules to make classes using Equatable more maintainable.', 23 | link: 'https://pub.dev/packages/equatable_lint', 24 | }, 25 | { 26 | name: 'Hardcoded Strings', 27 | description: 28 | 'Linter which notifies if the user is using hardcoded strings or invalid Ditto keys', 29 | link: 'https://pub.dev/packages/hardcoded_strings', 30 | }, 31 | ]; 32 | --- 33 | 34 | 35 |
36 |
39 |
40 | 58 |
59 |
60 |
61 |

62 | Plugin library 63 |

64 |
65 |
68 | { 69 | cards.map((card, index) => ( 70 | 7 ? 'js-hidden-cards hidden' : ''} {...card} /> 71 | )) 72 | } 73 |
74 | 79 | 84 |
85 | 93 |
94 |
95 |
96 |
98 |
99 |
100 | -------------------------------------------------------------------------------- /website/src/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'ITC Avant Garde Gothic Book'; 3 | src: url('../fonts/ITCAvantGardeRegular.woff') format('woff'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'ITC Avant Garde Gothic Book'; 10 | src: url('../fonts/ITCAvantGardePro-Demi.woff') format('woff'); 11 | font-weight: 600; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'ITC Avant Garde Gothic Book'; 17 | src: url('../fonts/ITCAvantGardePro-Md.woff') format('woff'); 18 | font-weight: 500; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: 'ITC Avant Garde Gothic Book'; 24 | src: url('../fonts/ITCAvantGardePro-Bk.woff') format('woff'); 25 | font-weight: 300; 26 | font-style: normal; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /website/src/styles/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #2E2174; 3 | --light-background: #fff; 4 | --dark-background: #0C0D12; 5 | 6 | --astro-code-color-text: #000; 7 | --astro-code-color-background: transparent; 8 | --astro-code-token-constant: #6ee7b7; 9 | --astro-code-token-string: #6ee7b7; 10 | --astro-code-token-comment: #71717a; 11 | --astro-code-token-keyword: #000; 12 | --astro-code-token-parameter: #f9a8d4; 13 | --astro-code-token-function: #c4b5fd; 14 | --astro-code-token-string-expression: rgba(71, 148, 23, 0.88); 15 | --astro-code-token-punctuation: #e4e4e7; 16 | --astro-code-token-link: var(--primary); 17 | 18 | --shiki-color-background: var(--astro-code-color-background); 19 | --shiki-color-text: var(--astro-code-color-text); 20 | --shiki-token-constant: var(--astro-code-token-constant); 21 | --shiki-token-string: var(--astro-code-token-string); 22 | --shiki-token-comment: var(--astro-code-token-comment); 23 | --shiki-token-keyword: var(--astro-code-token-keyword); 24 | --shiki-token-parameter: var(--astro-code-token-parameter); 25 | --shiki-token-function: var(--astro-code-token-function); 26 | --shiki-token-string-expression: var(--astro-code-token-string-expression); 27 | --shiki-token-punctuation: var(--astro-code-token-punctuation); 28 | } 29 | 30 | html { 31 | --background: var(--light-background); 32 | 33 | color-scheme: light; 34 | } 35 | 36 | html.dark { 37 | --background: var(--dark-background); 38 | 39 | --astro-code-color-text: #FFF; 40 | --astro-code-token-comment: #FFF; 41 | --astro-code-token-keyword: #FFF; 42 | 43 | color-scheme: dark; 44 | } 45 | 46 | html, 47 | body { 48 | height: 100%; 49 | } 50 | 51 | @keyframes fadeIn { 52 | 0% { 53 | opacity: 0; 54 | } 55 | 100% { 56 | opacity: 1; 57 | } 58 | } 59 | 60 | .fade-in { 61 | animation: fadeIn 0.2s ease-in-out; 62 | } 63 | 64 | @tailwind base; 65 | @tailwind components; 66 | @tailwind utilities; 67 | 68 | @layer base { 69 | .underline-gradient { 70 | @apply bg-gradient-to-r from-[currentColor] to-[currentColor] bg-[length:0_1px] bg-[0_100%] bg-no-repeat transition-[background-size] duration-500; 71 | } 72 | 73 | .underline-gradient-visible { 74 | @apply bg-[length:100%_1px]; 75 | } 76 | 77 | .underline-gradient-hidden { 78 | @apply bg-[length:0_1px]; 79 | } 80 | } 81 | 82 | @layer components { 83 | .btn-primary { 84 | @apply rounded-md bg-white px-6 py-4 text-center 85 | text-linter transition-opacity duration-500 ease-in-out 86 | hover:opacity-70 disabled:pointer-events-none disabled:cursor-default; 87 | } 88 | 89 | .btn-secondary { 90 | @apply btn-primary bg-linter text-white; 91 | } 92 | 93 | .btn-third { 94 | @apply btn-primary bg-transparent bg-[linear-gradient(93.73deg,#2D2072_28.69%,#4B1D62_88.7%)] text-white shadow-lg shadow-indigo-700/30; 95 | } 96 | } 97 | 98 | @layer utilities { 99 | /* Chrome, Safari and Opera */ 100 | .no-scrollbar::-webkit-scrollbar { 101 | display: none; 102 | } 103 | 104 | .no-scrollbar { 105 | -ms-overflow-style: none; /* IE and Edge */ 106 | scrollbar-width: none; /* Firefox */ 107 | } 108 | 109 | .overflow-initial { 110 | overflow: initial; 111 | } 112 | } -------------------------------------------------------------------------------- /website/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 4 | darkMode: 'class', 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | itc: '"ITC Avant Garde Gothic Book", Arial, sans-serif;', 9 | }, 10 | boxShadow: { 11 | footer: '0px -2px 227px rgba(0, 0, 0, 0.14)' 12 | }, 13 | colors: { 14 | linter: { 15 | DEFAULT: 'var(--primary)', 16 | background: 'var(--background)', 17 | card: '#2C1E3C', 18 | toggle: '#261F3C' 19 | }, 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | } -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true, 6 | // Enable top-level await, and other modern ESM features. 7 | "target": "ESNext", 8 | "module": "ESNext", 9 | // Enable node-style module resolution, for things like npm package imports. 10 | "moduleResolution": "node", 11 | // Enable JSON imports. 12 | "resolveJsonModule": true, 13 | // Enable stricter transpilation for better output. 14 | "isolatedModules": true, 15 | // Astro will directly run your TypeScript code, no transpilation needed. 16 | "noEmit": true, 17 | "jsx": "preserve", 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@components/*": [ 22 | "src/components/*" 23 | ], 24 | "@layouts/*": [ 25 | "src/layouts/*" 26 | ], 27 | "@images/*": [ 28 | "src/images/*" 29 | ] 30 | } 31 | } 32 | } --------------------------------------------------------------------------------