├── .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 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
8 | [](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 |
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 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/website/src/components/VsCodeBanner.astro:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
26 |
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 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 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {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 |
97 |
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 | }
--------------------------------------------------------------------------------