diff --git a/pkg/linter/.github/ISSUE_TEMPLATE/config.yml b/pkg/linter/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..97e6337d4bc --- /dev/null +++ b/pkg/linter/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Dart team recommended lints + url: https://github.com/dart-lang/lints/issues/new/choose + about: Request a lint for inclusion, exclusion or otherwise discuss Dart team recommended lints. diff --git a/pkg/linter/.github/ISSUE_TEMPLATE/lint-bug-report.md b/pkg/linter/.github/ISSUE_TEMPLATE/lint-bug-report.md new file mode 100644 index 00000000000..ca6a62be486 --- /dev/null +++ b/pkg/linter/.github/ISSUE_TEMPLATE/lint-bug-report.md @@ -0,0 +1,22 @@ +--- +name: Lint bug report +about: Report a bug with, or propose a modification to, an existing lint. +title: '' +labels: '' +assignees: '' + +--- + +**Describe the issue** +A clear and concise description of what the issue is. + +If you're seeing unexpected behavior in a lint rule, please include the name of the rule in the issue description. + +**To Reproduce** +Ideally, please include an example. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/pkg/linter/.github/ISSUE_TEMPLATE/lint-proposal.md b/pkg/linter/.github/ISSUE_TEMPLATE/lint-proposal.md new file mode 100644 index 00000000000..36c5205d347 --- /dev/null +++ b/pkg/linter/.github/ISSUE_TEMPLATE/lint-proposal.md @@ -0,0 +1,50 @@ +--- +name: Lint proposal +about: Propose a new lint rule or a modification to an existing one. +title: 'proposal: ``' +labels: 'lint proposal, status: pending' +assignees: '' + +--- + +## + +*Replace above with your proposed rule name. (See notes on naming in [Writing Lints].)* + +## Description + +*Provide a short description suitable for display in console output.* + +## Details + +*Give a detailed description that should provide context and motivation. Ideally this could be used directly in the rule's documentation.* + +## Kind + +*Does this enforce style advice? Guard against errors? Other?* + +## Bad Examples + +*Add a few examples that demonstrate where this lint should fire.* + +## Good Examples + +*Add a few examples that demonstrate a “good” adoption of this lint’s principle.* + +## Discussion + +*Add any other motivation or useful context here.* + +### Discussion checklist + +- [ ] List any existing rules this proposal modifies, complements, overlaps or conflicts with. +- [ ] List any relevant issues (reported here, the [SDK Tracker], or elsewhere). +- [ ] If there's any prior art (e.g., in other linters), please add references here. +- [ ] If this proposal corresponds to [Effective Dart] or [Flutter Style Guide] advice, please call it out. (If there isn't any corresponding advice, should there be?) +- [ ] If this proposal is motivated by real-world examples, please provide as many details as you can. Demonstrating potential impact is _especially valuable_. + + +[Writing Lints]: https://github.com/dart-lang/linter/blob/main/doc/writing-lints.md +[Effective Dart]: https://dart.dev/effective-dart +[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo +[SDK Tracker]: https://github.com/dart-lang/sdk/issues diff --git a/pkg/linter/.github/ISSUE_TEMPLATE/lint-quick-fix-request.md b/pkg/linter/.github/ISSUE_TEMPLATE/lint-quick-fix-request.md new file mode 100644 index 00000000000..0b4e97b22f3 --- /dev/null +++ b/pkg/linter/.github/ISSUE_TEMPLATE/lint-quick-fix-request.md @@ -0,0 +1,17 @@ +--- +name: Request a quick-fix for a lint +about: Request that a quick-fix be added for an existing lint. +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe the quick-fix** +Include as much detail as you can. + +**Examples** +Ideally, provide examples of the quick-fix applied. + +**Additional context** +Add any other considerations or context here. diff --git a/pkg/linter/.github/PULL_REQUEST_TEMPLATE.md b/pkg/linter/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..7194f6a6565 --- /dev/null +++ b/pkg/linter/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +# Description + +Please include a summary of the change and a reference to any corresponding issues. +If this is a fix to an existing lint rule or a proposed new one, please include +the name of the rule in the pull request. + +If this is a new lint or feature and there is not an existing tracking bug, please +consider opening one to better facilitate conversation. + +Fixes # (issue) diff --git a/pkg/linter/.github/dependabot.yaml b/pkg/linter/.github/dependabot.yaml new file mode 100644 index 00000000000..439e796b48d --- /dev/null +++ b/pkg/linter/.github/dependabot.yaml @@ -0,0 +1,10 @@ +# Dependabot configuration file. +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - autosubmit diff --git a/pkg/linter/.github/issue-labeler.yml b/pkg/linter/.github/issue-labeler.yml new file mode 100644 index 00000000000..f1186cfe852 --- /dev/null +++ b/pkg/linter/.github/issue-labeler.yml @@ -0,0 +1,10 @@ +# Auto-generated by `tool/labeler/issue_config.dart` + +set-core: + - '(avoid_empty_else|avoid_relative_lib_imports|avoid_shadowing_type_parameters|avoid_types_as_parameter_names|await_only_futures|camel_case_extensions|camel_case_types|collection_methods_unrelated_type|curly_braces_in_flow_control_structures|depend_on_referenced_packages|empty_catches|file_names|hash_and_equals|implicit_call_tearoffs|no_duplicate_case_values|non_constant_identifier_names|null_check_on_nullable_type_parameter|package_prefixed_library_names|prefer_generic_function_type_aliases|prefer_is_empty|prefer_is_not_empty|prefer_iterable_whereType|prefer_typing_uninitialized_variables|provide_deprecation_message|unnecessary_overrides|unrelated_type_equality_checks|use_string_in_part_of_directives|valid_regexps|void_checks)' + +set-recommended: + - '(annotate_overrides|avoid_function_literals_in_foreach_calls|avoid_init_to_null|avoid_null_checks_in_equality_operators|avoid_renaming_method_parameters|avoid_return_types_on_setters|avoid_returning_null_for_void|avoid_single_cascade_in_expression_statements|constant_identifier_names|control_flow_in_finally|empty_constructor_bodies|empty_statements|exhaustive_cases|implementation_imports|library_names|library_prefixes|library_private_types_in_public_api|no_leading_underscores_for_library_prefixes|no_leading_underscores_for_local_identifiers|null_closures|overridden_fields|package_names|prefer_adjacent_string_concatenation|prefer_collection_literals|prefer_conditional_assignment|prefer_contains|prefer_equal_for_default_values|prefer_final_fields|prefer_for_elements_to_map_fromIterable|prefer_function_declarations_over_variables|prefer_if_null_operators|prefer_initializing_formals|prefer_inlined_adds|prefer_interpolation_to_compose_strings|prefer_is_not_operator|prefer_null_aware_operators|prefer_spread_collections|prefer_void_to_null|recursive_getters|slash_for_doc_comments|type_init_formals|unnecessary_brace_in_string_interps|unnecessary_const|unnecessary_constructor_name|unnecessary_getters_setters|unnecessary_late|unnecessary_new|unnecessary_null_aware_assignments|unnecessary_null_in_if_null_operators|unnecessary_nullable_for_final_variable_declarations|unnecessary_string_escapes|unnecessary_string_interpolations|unnecessary_this|use_function_type_syntax_for_parameters|use_rethrow_when_possible)' + +set-flutter: + - '(avoid_print|avoid_unnecessary_containers|avoid_web_libraries_in_flutter|no_logic_in_create_state|prefer_const_constructors|prefer_const_constructors_in_immutables|prefer_const_declarations|prefer_const_literals_to_create_immutables|sized_box_for_whitespace|sort_child_properties_last|use_build_context_synchronously|use_full_hex_values_for_flutter_colors|use_key_in_widget_constructors)' diff --git a/pkg/linter/.github/labeler.yml b/pkg/linter/.github/labeler.yml new file mode 100644 index 00000000000..0375cabf7fe --- /dev/null +++ b/pkg/linter/.github/labeler.yml @@ -0,0 +1,104 @@ +# Auto-generated by `tool/labeler/pr_config.dart` + +set-core: + - lib/**/avoid_empty_else.dart + - lib/**/avoid_relative_lib_imports.dart + - lib/**/avoid_shadowing_type_parameters.dart + - lib/**/avoid_types_as_parameter_names.dart + - lib/**/await_only_futures.dart + - lib/**/camel_case_extensions.dart + - lib/**/camel_case_types.dart + - lib/**/collection_methods_unrelated_type.dart + - lib/**/curly_braces_in_flow_control_structures.dart + - lib/**/depend_on_referenced_packages.dart + - lib/**/empty_catches.dart + - lib/**/file_names.dart + - lib/**/hash_and_equals.dart + - lib/**/implicit_call_tearoffs.dart + - lib/**/no_duplicate_case_values.dart + - lib/**/non_constant_identifier_names.dart + - lib/**/null_check_on_nullable_type_parameter.dart + - lib/**/package_prefixed_library_names.dart + - lib/**/prefer_generic_function_type_aliases.dart + - lib/**/prefer_is_empty.dart + - lib/**/prefer_is_not_empty.dart + - lib/**/prefer_iterable_whereType.dart + - lib/**/prefer_typing_uninitialized_variables.dart + - lib/**/provide_deprecation_message.dart + - lib/**/unnecessary_overrides.dart + - lib/**/unrelated_type_equality_checks.dart + - lib/**/use_string_in_part_of_directives.dart + - lib/**/valid_regexps.dart + - lib/**/void_checks.dart + +set-recommended: + - lib/**/annotate_overrides.dart + - lib/**/avoid_function_literals_in_foreach_calls.dart + - lib/**/avoid_init_to_null.dart + - lib/**/avoid_null_checks_in_equality_operators.dart + - lib/**/avoid_renaming_method_parameters.dart + - lib/**/avoid_return_types_on_setters.dart + - lib/**/avoid_returning_null_for_void.dart + - lib/**/avoid_single_cascade_in_expression_statements.dart + - lib/**/constant_identifier_names.dart + - lib/**/control_flow_in_finally.dart + - lib/**/empty_constructor_bodies.dart + - lib/**/empty_statements.dart + - lib/**/exhaustive_cases.dart + - lib/**/implementation_imports.dart + - lib/**/library_names.dart + - lib/**/library_prefixes.dart + - lib/**/library_private_types_in_public_api.dart + - lib/**/no_leading_underscores_for_library_prefixes.dart + - lib/**/no_leading_underscores_for_local_identifiers.dart + - lib/**/null_closures.dart + - lib/**/overridden_fields.dart + - lib/**/package_names.dart + - lib/**/prefer_adjacent_string_concatenation.dart + - lib/**/prefer_collection_literals.dart + - lib/**/prefer_conditional_assignment.dart + - lib/**/prefer_contains.dart + - lib/**/prefer_equal_for_default_values.dart + - lib/**/prefer_final_fields.dart + - lib/**/prefer_for_elements_to_map_fromIterable.dart + - lib/**/prefer_function_declarations_over_variables.dart + - lib/**/prefer_if_null_operators.dart + - lib/**/prefer_initializing_formals.dart + - lib/**/prefer_inlined_adds.dart + - lib/**/prefer_interpolation_to_compose_strings.dart + - lib/**/prefer_is_not_operator.dart + - lib/**/prefer_null_aware_operators.dart + - lib/**/prefer_spread_collections.dart + - lib/**/prefer_void_to_null.dart + - lib/**/recursive_getters.dart + - lib/**/slash_for_doc_comments.dart + - lib/**/type_init_formals.dart + - lib/**/unnecessary_brace_in_string_interps.dart + - lib/**/unnecessary_const.dart + - lib/**/unnecessary_constructor_name.dart + - lib/**/unnecessary_getters_setters.dart + - lib/**/unnecessary_late.dart + - lib/**/unnecessary_new.dart + - lib/**/unnecessary_null_aware_assignments.dart + - lib/**/unnecessary_null_in_if_null_operators.dart + - lib/**/unnecessary_nullable_for_final_variable_declarations.dart + - lib/**/unnecessary_string_escapes.dart + - lib/**/unnecessary_string_interpolations.dart + - lib/**/unnecessary_this.dart + - lib/**/use_function_type_syntax_for_parameters.dart + - lib/**/use_rethrow_when_possible.dart + +set-flutter: + - lib/**/avoid_print.dart + - lib/**/avoid_unnecessary_containers.dart + - lib/**/avoid_web_libraries_in_flutter.dart + - lib/**/no_logic_in_create_state.dart + - lib/**/prefer_const_constructors.dart + - lib/**/prefer_const_constructors_in_immutables.dart + - lib/**/prefer_const_declarations.dart + - lib/**/prefer_const_literals_to_create_immutables.dart + - lib/**/sized_box_for_whitespace.dart + - lib/**/sort_child_properties_last.dart + - lib/**/use_build_context_synchronously.dart + - lib/**/use_full_hex_values_for_flutter_colors.dart + - lib/**/use_key_in_widget_constructors.dart diff --git a/pkg/linter/.github/workflows/build.yaml b/pkg/linter/.github/workflows/build.yaml new file mode 100644 index 00000000000..5f8150c13d3 --- /dev/null +++ b/pkg/linter/.github/workflows/build.yaml @@ -0,0 +1,48 @@ +name: linter + +on: + schedule: + # “At 00:00 (UTC) on Sunday.” + - cron: '0 0 * * 0' + pull_request: + push: + branches: + - main + +env: + PUB_ENVIRONMENT: bot.github + +permissions: read-all + +jobs: + build: + permissions: + checks: write # for coverallsapp/github-action to create new checks + name: ${{ matrix.linter-bot }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + linter-bot: + - main + - coverage + - benchmark + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + with: + sdk: dev + + - run: dart pub get + + - name: tool/bot.sh + env: + LINTER_BOT: ${{ matrix.linter-bot }} + run: ./tool/bot.sh + - name: upload coverage + if: matrix.linter-bot == 'coverage' + uses: coverallsapp/github-action@95b1a2355bd0e526ad2fd62da9fd386ad4c98474 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: var/lcov.info diff --git a/pkg/linter/.github/workflows/issue-labeler.yml b/pkg/linter/.github/workflows/issue-labeler.yml new file mode 100644 index 00000000000..a679f086bd5 --- /dev/null +++ b/pkg/linter/.github/workflows/issue-labeler.yml @@ -0,0 +1,21 @@ +name: "issue labeler" + +on: + issues: + types: [opened, edited] + +permissions: read-all + +jobs: + triage: + permissions: + contents: read + issues: write + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@98b5412841f6c4b0b3d9c29d53c13fad16bd7de2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/issue-labeler.yml + enable-versioned-regex: 0 + include-title: 1 diff --git a/pkg/linter/.github/workflows/labeler.yml b/pkg/linter/.github/workflows/labeler.yml new file mode 100644 index 00000000000..16368696fcd --- /dev/null +++ b/pkg/linter/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +name: "labeler" + +on: + - pull_request_target + +permissions: read-all + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/pkg/linter/.github/workflows/no-response.yml b/pkg/linter/.github/workflows/no-response.yml new file mode 100644 index 00000000000..dd7bbbc0048 --- /dev/null +++ b/pkg/linter/.github/workflows/no-response.yml @@ -0,0 +1,35 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 + with: + days-before-stale: -1 + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution! diff --git a/pkg/linter/.github/workflows/scorecards-analysis.yml b/pkg/linter/.github/workflows/scorecards-analysis.yml new file mode 100644 index 00000000000..ac54b949ab4 --- /dev/null +++ b/pkg/linter/.github/workflows/scorecards-analysis.yml @@ -0,0 +1,54 @@ +name: Scorecards supply-chain security +on: + # Only the default branch is supported. + branch_protection_rule: + push: + branches: [ main ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + id-token: write + actions: read + contents: read + + steps: + - name: "Checkout code" + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 + with: + results_file: results.sarif + results_format: sarif + # Read-only PAT token. To create it, + # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, + # regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). + - name: "Upload artifact" + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 + with: + sarif_file: results.sarif diff --git a/pkg/linter/.gitignore b/pkg/linter/.gitignore new file mode 100644 index 00000000000..0454e793417 --- /dev/null +++ b/pkg/linter/.gitignore @@ -0,0 +1,13 @@ +.idea +.dart_tool +packages +build +.project +.settings +pubspec.lock +.packages +linter.iml +.atom +/.vscode +/.history +/doc/lints/ diff --git a/pkg/linter/AUTHORS b/pkg/linter/AUTHORS new file mode 100644 index 00000000000..bc0ed99938a --- /dev/null +++ b/pkg/linter/AUTHORS @@ -0,0 +1,50 @@ +# Names should be added to this file with this pattern: +# +# For individuals: +# Name +# +# For organizations: +# Organization +# + +Google Inc. <*@google.com> + +Alexandre Ardhuin +Ahmed Ashour +Honza Bittner +Patrice Chalin +Maxim Dikun +Bernardo Ferrari +David Fong +Xavier Hainaux +Alexey Knyazev +Jawahar +Keisuke Kishida +Benjamin Li +Guillermo López-Anglada +Parker Lougheed +David Martos +Juan Pablo Paulsen +Mathieu Payette +Luke Pighetti +Sem van Rij +Michael Joseph Rosenthal +Muhammed Salih Guler +Lucas Saudon +Nolan Scobie +TAKAHASHI Shuuji +Cameron Steffen +Danny Tuppeny +Ivan Tugay +Rohan Vanheusden +Minh Dao +Sai Sandeep Mutyala +Kristen McWilliam +Paul Köhler +Felipe Morschel +Nils Reichardt +Alex Li +Michail Smirnov +Peter Leibiger +Modestas Valauskas +CCXXXI diff --git a/pkg/linter/CHANGELOG.md b/pkg/linter/CHANGELOG.md new file mode 100644 index 00000000000..42b6e1b8702 --- /dev/null +++ b/pkg/linter/CHANGELOG.md @@ -0,0 +1,1437 @@ +# 3.1.0-wip + +- new lint: `no_self_assignments` +- new lint: `no_wildcard_variable_uses` + +# 3.0.0 + +- new lint: `implicit_reopen` +- new lint: `unnecessary_breaks` +- new lint: `type_literal_in_constant_pattern` +- new lint: `invalid_case_patterns` +- new lint: `matching_super_parameters` +- new lint: `no_literal_bool_comparisons` + +- removed lint: `enable_null_safety` +- removed lint: `invariant_booleans` +- removed lint: `prefer_bool_in_asserts` +- removed lint: `prefer_equal_for_default_values` +- removed lint: `super_goes_last` + +--- + +# 1.35.0 + +- add new lints: + - `implicit_reopen` + - `unnecessary_breaks` + - `type_literal_in_constant_pattern` +- updates to existing lints to support patterns and class modifiers +- remove support for: + - `enable_null_safety` + - `invariant_booleans` + - `prefer_bool_in_asserts` + - `prefer_equal_for_default_values` + - `super_goes_last` +- fix `unnecessary_parenthesis` false-positives with null-aware expressions +- fix `void_checks` to allow assignments of `Future?` to parameters + typed `FutureOr?` +- fix `use_build_context_synchronously` in if conditions +- fix a false positive for `avoid_private_typedef_functions` with generalized + type aliases +- update `unnecessary_parenthesis` to detect some doubled parens +- update `void_checks` to allow returning `Never` as void +- update `no_adjacent_strings_in_list` to support set literals and for- and + if-elements +- update `avoid_types_as_parameter_names` to handle type variables +- update `avoid_positional_boolean_parameters` to handle typedefs +- update `avoid_redundant_argument_values` to check parameters of redirecting + constructors +- improve performance for `prefer_const_literals_to_create_immutables` +- update `use_build_context_synchronously` to check context properties +- improve `unnecessary_parenthesis` support for property accesses and method + invocations + +# 1.34.0 + +- update `only_throw_errors` to not report on values of type `Never` +- update `prefer_mixin` to handle class mixins +- update `unnecessary_null_checks` to ignore `Future.value` and + `Completer.complete` +- fix `unnecessary_parenthesis` false positives on constant patterns +- new lint: `invalid_case_patterns` +- update `unnecessary_const` to handle case patterns +- improve handling of null-aware cascades in `unnecessary_parenthesis` +- update `unreachable_from_main` to report unreachable public static fields, + getters, setters, and methods, that are declared on public classes, mixins, + enums, and extensions + +# 1.33.0 + +- fix `unnecessary_parenthesis` false-positive with null-aware expressions +- fix `void_checks` to allow assignments of `Future?` to parameters + typed `FutureOr?` +- removed support for: + - `enable_null_safety` + - `invariant_booleans` + - `prefer_bool_in_asserts` + - `prefer_equal_for_default_values` + - `super_goes_last` +- update `unnecessary_parenthesis` to detect some doubled parens +- update `void_checks` to allow returning `Never` as void +- new lint: `unnecessary_breaks` +- fix `use_build_context_synchronously` in if conditions +- update `no_adjacent_strings_in_list` to support set literals and for- and + if-elements + +# 1.32.0 + +- update `avoid_types_as_parameter_names` to handle type variables +- update `avoid_positional_boolean_parameters` to handle typedefs +- improve `unnecessary_parenthesis` support for property accesses and method invocations +- update `avoid_redundant_argument_values` to check parameters of redirecting constructors +- performance improvements for `prefer_const_literals_to_create_immutables` +- update `use_build_context_synchronously` to check context properties +- fix false positive for `avoid_private_typedef_functions` with generalized type aliases + +# 1.31.0 + +- update `prefer_equal_for_default_values` to not report for SDKs `>=2.19`, + where this lint is now an analyzer diagnostic. +- update `unrelated_type_equality_checks` to support updated `package:fixnum` + structure. + +# 1.30.0 + +- new lint: `enable_null_safety` +- new lint: `library_annotations` +- miscellaneous documentation improvements + +# 1.29.0 + +- new lint: `dangling_library_doc_comments` +- fix `no_leading_underscores_for_local_identifiers` to not report super formals as local variables +- fix `unnecessary_overrides` false negatives +- fix `cancel_subscriptions` for nullable fields +- new lint: `collection_methods_unrelated_type` +- update `library_names` to support unnamed libraries +- fix `unnecessary_parenthesis` support for as-expressions +- fix `use_build_context_synchronously` to check for context property accesses +- fix false positive in `comment_references` +- improved unrelated type checks to handle enums and cascades +- fix `unnecessary_brace_in_string_interps` for `this` expressions +- update `use_build_context_synchronously` for `BuildContext.mounted` +- improve `flutter_style_todos` to handle more cases +- fix `use_build_context_synchronously` to check for `BuildContext`s in named expressions +- fix `exhaustive_cases` to check parenthesized expressions +- performance improvements for: + - `avoid_null_checks_in_equality_operators` + - `join_return_with_statement` + - `recursive_getters` + - `unnecessary_lambdas` + - `diagnostic_describe_all_properties` + - `prefer_foreach` + - `avoid_escaping_inner_quotes` + - `cascade_invocations` + - `tighten_type_of_initializing_formals` + - `prefer_interpolation_to_compose_strings` + - `prefer_constructors_over_static_methods` + - `avoid_returning_null` + - `parameter_assignments` + - `prefer_constructors_over_static_methods` + - `prefer_interpolation_to_compose_strings` + - `avoid_returning_null` + - `avoid_returning_this` + - `flutter_style_todos` + - `avoid_positional_boolean_parameters` + - `prefer_const_constructors` +- new lint: `implicit_call_tearoffs` +- new lint: `unnecessary_library_directive` + +# 1.28.0 + +- update `avoid_redundant_argument_values` to work with enum declarations +- performance improvements for `prefer_contains` +- new lint: `unreachable_from_main` +- (internal): analyzer API updates and `DartTypeUtilities` refactoring + +# 1.27.0 + +- fix `avoid_redundant_argument_values` when referencing required + parameters in legacy libraries +- performance improvements for `use_late_for_private_fields_and_variables` +- new lint: `use_string_in_part_of_directives` +- fixed `use_super_parameters` false positive with repeated super + parameter references +- updated `use_late_for_private_fields_and_variables` to handle enums +- fixed `prefer_contains` false positive when start index is non-zero +- improved `noop_primitive_operations` to catch `.toString()` + in string interpolations +- updated `public_member_api_docs` to report diagnostics on extension + names (instead of bodies) +- miscellaneous documentation improvements +- (internal): `DartTypeUtilities` refactoring + +# 1.26.0 + +- new lint: `combinators_ordering` +- fixed `use_colored_box` and `use_decorated_box` to not over-report + on containers without a child +- fixed false positive for `unnecessary_parenthesis` on a map-or-set + literal at the start of an expression statement +- fixed false positive for `prefer_final_locals` reporting on fields +- fixed `unnecessary_overrides` to allow overrides on `@Protected` + members +- fixed `avoid_multiple_declarations_per_line` false positive in + `for` statements +- fixed `prefer_final_locals` false positive on declaration lists + with at least one non-final variable +- fixed `use_build_context_synchronously` to handle `await`s in + `if` conditions + +# 1.25.0 + +- new lint: `discarded_futures` +- improved message and highlight range for + `no_duplicate_case_values` +- performance improvements for `lines_longer_than_80_chars`, + `prefer_const_constructors_in_immutables`, and + `prefer_initializing_formals` + +# 1.24.0 + +- fix `prefer_final_parameters` to support super parameters +- new lint: `unnecessary_to_list_in_spreads` +- fix `unawaited_futures` to handle string interpolated + futures +- update `use_colored_box` to not flag nullable colors +- new lint: `unnecessary_null_aware_operator_on_extension_on_nullable` + +# 1.23.0 + +- fixed `no_leading_underscores_for_local_identifiers` + to lint local function declarations +- fixed `avoid_init_to_null` to correctly handle super + initializing defaults that are non-null +- updated `no_leading_underscores_for_local_identifiers` + to allow identifiers with just underscores +- fixed `flutter_style_todos` to support usernames that + start with a digit +- updated `require_trailing_commas` to handle functions + in asserts and multi-line strings +- updated `unsafe_html` to allow assignments to + `img.src` +- fixed `unnecessary_null_checks` to properly handle map + literal entries + +# 1.22.0 + +- fixed false positives for `unnecessary_getters_setters` + and `prefer_final_fields`with enhanced enums +- updated to analyzer 3.4.0 APIs +- fixed null-safe variance in `invariant_booleans` + +# 1.21.2 + +- several `use_super_parameters` false positive fixes +- updated `depend_on_referenced_packages` to treat + `flutter_gen` as a virtual package, not needing an + explicit dependency + +# 1.21.1 + +- bumped language lower-bound constraint to `2.15.0` + +# 1.21.0 + +- fixed `use_key_in_widget_constructors` false positive + with `key` super parameter initializers +- fixed `use_super_parameters` false positive with field + formal params +- updated `unnecessary_null_checks` and + `null_check_on_nullable_type_parameter` to handle + list/set/map literals, and `yield` and `await` + expressions + +# 1.20.0 + +- renamed `use_super_initializers` to `use_super_parameters` +- fixed `unnecessary_null_aware_assignments` property-access + false positive + +# 1.19.2 + +- new lint: `use_super_initializers` +- new lint: `use_enums` +- new lint: `use_colored_box` +- performance improvements for `sort_constructors` +- doc improvements for `always_use_package_imports`, + `avoid_print`, and `avoid_relative_lib_imports` +- update `avoid_void_async` to skip `main` functions +- update `prefer_final_parameters` to not super on super params +- lint updates for enhanced-enums and super-initializer language + features +- updated `unnecessary_late` to report on the variable name +- marked `null_check_on_nullable_type_parameter` stable + +# 1.18.0 + +- extend `camel_case_types` to cover enums +- fix `no_leading_underscores_for_local_identifiers` to not + mis-flag field formal parameters with default values +- fix `prefer_function_declarations_over_variables` to not + mis-flag non-final fields +- performance improvements for `prefer_contains` +- update `exhaustive_cases` to skip deprecated values that + redirect to other values + +# 1.17.1 + +- update to `analyzer` version 3.0 + +# 1.17.0 + +- new lint: `unnecessary_late` +- fix to `no_leading_underscores_for_local_identifiers` to allow + underscores in catch clauses + +# 1.16.0 + +- doc improvements for `prefer_initializing_formals` +- updates to `secure_pubspec_urls` to check `issue_tracker` and + `repository` entries +- new lint: `conditional_uri_does_not_exist` +- performance improvements for + `missing_whitespace_between_adjacent_strings` + +# 1.15.0 + +- new lint: `use_decorated_box` +- new lint: `no_leading_underscores_for_library_prefixes` +- new lint: `no_leading_underscores_for_local_identifiers` +- new lint: `secure_pubspec_urls` +- new lint: `sized_box_shrink_expand` +- new lint: `avoid_final_parameters` +- improved docs for `omit_local_variable_types` + +# 1.14.0 + +- fix `omit_local_variable_types` to not flag a local type that is + required for inference + +# 1.13.0 + +- allow `while (true) { ...}` in `literal_only_boolean_expressions` +- fixed `file_names` to report at the start of the file (not the entire + compilation unit) +- fixed `prefer_collection_literals` named typed param false positive +- control flow improvements for `use_build_context_synchronously` + +# 1.12.0 + +- fixed `unnecessary_lambdas` false positive for const constructor invocations +- updated `avoid_print` to allow `kDebugMode`-wrapped print calls +- fixed handling of initializing formals in `prefer_final_parameters` +- fixed `unnecessary_parenthesis` false positive with function expressions + +# 1.11.0 + +- added support for constructor tear-offs to `avoid_redundant_argument_values`, + `unnecessary_lambdas`, and `unnecessary_parenthesis` +- new lint: `unnecessary_constructor_name` to flag unnecessary uses of `.new` + +# 1.10.0 + +- improved regular expression parsing performance for common checks + (`camel_case_types`, `file_names`, etc.) +- (internal) migrated to analyzer 2.1.0 APIs +- fixed false positive in `use_build_context_synchronously` in awaits inside + anonymous functions +- fixed `overridden_fields` false positive w/ static fields +- fixed false positive in `avoid_null_checks_in_equality_operators` w/ + non-nullable params +- fixed false positive for deferred imports in `prefer_const_constructors` + +# 1.9.0 + +- marked `avoid_dynamic_calls` stable +- (internal) removed unused `MockPubVisitor` and `MockRule` classes +- fixed `prefer_void_to_null` false positive w/ overridden properties +- (internal) removed references to `NodeLintRule` in lint rule declarations +- fixed `prefer_void_to_null` false positive on overriding returns +- fixed `prefer_generic_function_type_aliases` false positive w/ incomplete statements +- fixed false positive for `prefer_initializing_formals` with factory constructors +- fixed `void_checks` false positives with incomplete source +- updated `unnecessary_getters_setters` to only flag the getter +- improved messages for `avoid_renaming_method_parameters` +- fixed false positive in `prefer_void_to_null` +- fixed false positive in `omit_local_variable_types` +- fixed false positive in `use_rethrow_when_possible` +- performance improvements for `annotate_overrides`, `prefer_contains`, and `prefer_void_to_null` + +# 1.8.0 + +- performance improvements for `prefer_is_not_empty` +- fixed false positive in `no_logic_in_create_state` +- improve `package_names` to allow dart identifiers as package names +- fixed false-positive in `package_names` (causing keywords to wrongly get flagged) +- fixed `avoid_classes_with_only_static_member` to check for inherited members and also + flag classes with only methods +- fixed `curly_braces_in_flow_control_structures` to properly flag terminating `else-if` + blocks +- improved `always_specify_types` to support type aliases +- fixed false positive in `unnecessary_string_interpolations` w/ nullable interpolated + strings +- fixed false positive in `avoid_function_literals_in_foreach_calls` for nullable + iterables +- fixed false positive in `avoid_returning_null` w/ NNBD +- fixed false positive in `use_late_for_private_fields_and_variables` in the presence + of const constructors +- new lint: `eol_at_end_of_file` +- updated `analyzer` constraint to `>=2.0.0 <3.0.0` + +# 1.7.1 + +- Update `analyzer` constraint to `>=1.7.0 <3.0.0`. +- Update `meta` constraint to `>=1.3.0 <3.0.0`. + +# 1.7.0 + +- fixed case-sensitive false positive in `use_full_hex_values_for_flutter_colors` +- improved try-block and switch statement flow analysis for + `use_build_context_synchronously` +- updated `use_setters_to_change_properties` to only highlight a method name, + not the entire body and doc comment +- updated `unnecessary_getters_setters` to allow otherwise "unnecessary" getters + and setters with annotations +- updated `missing_whitespace_between_adjacent_strings` to allow String + interpolations at the beginning and end of String literals +- updated `unnecessary_getters_setters` to allow for setters with non-basic + assignments (for example, `??=` or `+=`) + +# 1.6.1 + +- reverted relaxation of `sort_child_properties_last` to allow for a + trailing Widget in instance creations + +# 1.6.0 + +- relaxed `non_constant_identifier_names` to allow for a trailing + underscore +- fixed false negative in `prefer_final_parameters` where first parameter + is final +- improved `directives_ordering` sorting of directives with dot paths and + dot-separated package names +- relaxed `sort_child_properties_last` to allow for a trailing Widget in + instance creations + +# 1.5.0 + +- (internal) migrated to `SecurityLintCode` instead of deprecated + `SecurityLintCodeWithUniqueName` +- (internal) fixed `avoid_types_as_parameter_names` to skip field formal + parameters +- fixed false positives in `prefer_interpolation_to_compose_strings` where + the left operand is not a String +- fixed false positives in `only_throw_errors` for misidentified type + variables +- new lint: `depend_on_referenced_packages` +- update `avoid_returning_null_for_future` to skip checks for null-safe + libraries +- new lint: `use_test_throws_matchers` +- relax `sort_child_properties_last` to accept closures after child +- performance improvements for `prefer_contains` and `prefer_is_empty` +- new lint: `noop_primitive_operations` +- mark `avoid_web_libraries_in_flutter` as stable +- new lint: `prefer_final_parameters` +- update `prefer_initializing_formals` to allow assignments where identifier + names don't match + +# 1.4.0 + +- `directives_ordering` now checks ordering of `package:` imports in code + outside pub packages +- simple reachability analysis added to `use_build_context_synchronously` to + short-circuit await-discovery in terminating blocks +- `use_build_context_synchronously` updated to recognize nullable types when + accessed from legacy libraries + +# 1.3.0 + +- `non_constant_identifier_names` updated to check local variables, for-loop + initializers and catch clauses +- error range of `lines_longer_than_80_chars` updated to start at 80 to make + splitting easier +- new lint: `require_trailing_commas` +- new lint: `prefer_null_aware_method_calls` + +# 1.2.1 + +- fix: adjusted SDK lower bound to 2.12.0-0 (from 2.13.0-0) + +# 1.2.0 + +- improvements to `iterable_contains_unrelated_type` to better support `List` + content checks +- fixes to `camel_case_types` and `prefer_mixin` to support non-function + type aliases + +# 1.1.0 + +- fixed `prefer_mixin` to properly make exceptions for `dart.collection` legacy + mixins +- improved formatting of source examples in docs +- new lint: `use_build_context_synchronously` (experimental) +- new lint: `avoid_multiple_declarations_per_line` + +# 1.0.0 + +- full library migration to null-safety +- new lint: `use_if_null_to_convert_nulls_to_bools` +- new lint: `deprecated_consistency` +- new lint: `use_named_constants` +- deprecation of `avoid_as` + +# 0.1.129 + +- fixed a bug where `avoid_dynamic_calls` produced false-positives for `.call()` + +# 0.1.128 + +- new lint: `avoid_dynamic_calls` +- (internal): updated `avoid_type_to_string` to use `addArgumentList` registry API +- documentation improvements + +# 0.1.127 + +- fixed crash in `prefer_collection_literals` when there is no static parameter + element + +# 0.1.126 + +- fixed false negatives for `prefer_collection_literals` when a LinkedHashSet or + LinkedHashMap instantiation is passed as the argument to a function in any + position other than the first +- fixed false negatives for `prefer_collection_literals` when a LinkedHashSet or + LinkedHashMap instantiation is used in a place with a static type other than + Set or Map + +# 0.1.125 + +- (internal): update to new `PhysicalResourceProvider` API + +# 0.1.124 + +- fixed false positives in `prefer_constructors_over_static_methods` +- updated `package_names` to allow leading underscores + +# 0.1.123 + +- fixed NPEs in `unnecessary_null_checks` + +# 0.1.122 + +- fixed NPE in `unnecessary_null_checks` +- fixed NPE in `missing_whitespace_between_adjacent_strings` +- updated `void_checks` for NNBD +- fixed range error in `unnecessary_string_escapes` +- fixed false positives in `unnecessary_null_types` +- fixed `prefer_constructors_over_static_methods` to respect type parameters +- updated `always_require_non_null_named_parameters` to be NNBD-aware +- updated `unnecessary_nullable_for_final_variable_declarations` to allow dynamic +- update `overridden_fields` to not report on abstract parent fields +- fixes to `unrelated_type_equality_checks` for NNBD +- improvement to `type_init_formals`to allow types not equal to the field type + +# 0.1.121 + +- performance improvements to `always_use_package_imports`, `avoid_renaming_method_parameters`, `prefer_relative_imports` and `public_member_api_docs` +- (internal): update to analyzer `0.40.4` APIs + +# 0.1.120 + +- new lint: `cast_nullable_to_non_nullable` +- new lint: `null_check_on_nullable_type_parameter` +- new lint: `tighten_type_of_initializing_formals` +- update `public_member_apis` to check generic type aliases +- (internal): update to new analyzer APIs + +# 0.1.119 + +- fix `close_sinks` to handle `this`-prefixed property accesses +- new lint: `unnecessary_null_checks` +- fix `unawaited_futures` to handle `Future` subtypes +- new lint: `avoid_type_to_string` + +# 0.1.118 + +- new lint: `unnecessary_nullable_for_final_variable_declarations` +- fixed NPE in `prefer_asserts_in_initializer_lists` +- fixed range error in `unnecessary_string_escapes` +- `unsafe_html` updated to support unique error codes +- updates to `diagnostic_describe_all_properties` to check for `Diagnosticable`s (not `DiagnosticableMixin`s) +- new lint: `use_late` +- fixed `unnecessary_lambdas` to respect deferred imports +- updated `public_member_api_docs` to check mixins +- updated `unnecessary_statements` to skip `as` expressions +- fixed `prefer_relative_imports` to work with path dependencies + +# 0.1.117 + +- fixed `directives_ordering` to remove third party package special-casing +- fixed `unnecessary_lambdas` to check for tearoff assignability +- fixed `exhaustive_cases` to not flag missing cases that are defaulted +- fixed `prefer_is_empty` to special-case assert initializers and const contexts +- test utilities moved to: `lib/src/test_utilities` +- new lint: `do_not_use_environment` + +# 0.1.116 + +- new lint: `no_default_cases` (experimental) +- new lint: `exhaustive_cases` +- updated `type_annotate_public_apis` to allow inferred types in final field assignments +- updated `prefer_mixin` to allow "legacy" SDK abstract class mixins +- new lint: `use_is_even_rather_than_modulo` +- update `unsafe_html` to use a `SecurityLintCode` (making it un-ignorable) +- improved `sized_box_for_whitespace` to address false-positives + +# 0.1.115 + +- updated `avoid_types_as_parameter_names` to check catch-clauses +- fixed `unsafe_html` to check attributes and methods on extensions +- extended `unsafe_html` to include `Window.open`, `Element.html` and `DocumentFragment.html` in unsafe API checks +- improved docs for `sort_child_properties_last` +- (internal) `package:analyzer` API updates +- new lint: `sized_box_for_whitespace` + +# 0.1.114 + +- fixed `avoid_shadowing_type_parameters` to support extensions and mixins +- updated `non_constant_identifier_names` to allow named constructors made up of only underscores (`_`) +- updated `avoid_unused_constructor_parameters` to ignore unused params named in all underscores (`_`) + +# 0.1.113 + +- updated documentation links +- `one_member_abstracts` updated to not lint classes with mixins or implementing interfaces +- `unnecessary_getters_setters` fixed to ignore cases where a getter/setter is deprecated +- new lint: `leading_newlines_in_multiline_strings` +- improved highlight ranges for `avoid_private_typedef_functions` and `avoid_returning_null_for_future` + +# 0.1.112 + +- marked `prefer_typing_uninitialized_variables` and `omit_local_variable_types` as compatible + +# 0.1.111+1 + +- new lint: `use_raw_strings` +- new lint: `unnecessary_raw_strings` +- new lint: `avoid_escaping_inner_quotes` +- new lint: `unnecessary_string_escapes` +- incompatible rule documentation improvements + +# 0.1.110 + +- fixed flutter web plugin detection in `avoid_web_libraries_in_flutter` +- new lint: `unnecessary_string_interpolations` +- new lint: `missing_whitespace_between_adjacent_strings` +- `avoid_unused_constructor_parameters` updated to ignore deprecated parameters +- new lint: `no_runtimeType_toString` +- miscellaneous doc fixes + +# 0.1.109 + +- improved`prefer_single_quotes` lint message +- `unnecessary_finals` fixed to not flag fields +- `unnecessary_lambdas` fixed to work with type arguments +- (internal) migrated to use analyzer `LinterContext.resolveNameInScope()` API + +# 0.1.108 + +- fixes to `avoid_redundant_argument_values` +- new lint: `use_key_in_widget_constructors` +- `always_put_required_parameters` updated for NNBD +- updated to `package:analyzer` 0.39.3 APIs + +# 0.1.107 + +- miscellaneous doc cleanup (typos, etc) +- new lint: `avoid_redundant_argument_values` +- updated `slash_for_doc_comments` to check mixin declarations +- (internal) updates to use new `LinterContext.evaluateConstant` API +- improved docs for `always_require_non_null_named_parameters` + +# 0.1.106 + +- improved docs for `comment_references` +- fixed `null_closures` to properly handle `Iterable.singleWhere` +- (internal) migrated to latest analyzer APIs +- new lint: `no_logic_in_create_state` + +# 0.1.105+1 + +- fixed regressions in `always_require_non_null_named_parameters` +- (internal) pedantic lint clean-up + +# 0.1.105 + +- hardened check for lib dir location (fixing crashes in `avoid_renaming_method_parameters`, + `prefer_relative_imports` and `public_member_api_docs`) +- improved performance for `always_require_non_null_named_parameters` + +# 0.1.104 + +- updated `unnecessary_overrides` to allow overrides when annotations (besides `@override` are specified) +- updated `file_names` to allow names w/ leading `_`'s (and improved performance) +- new lint: `unnecessary_final` + +# 0.1.103 + +- updated `prefer_relative_imports` to use a faster and more robust way to check for self-package references +- updated our approach to checking for `lib` dir contents (speeding up `avoid_renaming_method_parameters` and + making `prefer_relative_imports` and `public_member_api_docs` amenable to internal package formats -- w/o pubspecs) + +# 0.1.102 + +- `avoid_web_libraries_in_flutter` updated to disallow access from all but Flutter web plugin packages +- updated `avoid_returning_null_for_void` to check only `null` literals (and not expressions having `Null` types) +- fixed `prefer_final_fields` to respect non-mutating prefix operators +- new lint: `prefer_is_not_operator` +- new lint: `avoid_unnecessary_containers` +- added basic nnbd-awareness to `avoid_init_to_null` + +# 0.1.101 + +- fixed `diagnostic_describe_all_properties` to flag properties in `Diagnosticable`s with no debug methods defined +- fixed `noSuchMethod` exception in `camel_case_extensions` when analyzing unnamed extensions +- fixed `avoid_print` to catch tear-off usage +- new lint: `avoid_web_libraries_in_flutter` (experimental) +- (internal) prepare `unnecessary_lambdas` for coming `MethodInvocation` vs. `FunctionExpressionInvocation` changes + +# 0.1.100 + +- (internal) stop accessing `staticType` in favor of getting type of `FormalParameter`s from the declared element +- (internal) remove stale analyzer work-around for collecting `TypeParameterElement`s in `prefer_const_constructors` + +# 0.1.99 + +- fixed unsafe cast in `overridden_fields` +- (internal) migrated to the mock SDK in `package:analyzer` for testing +- fixed empty argument list access in `use_full_hex_values_for_flutter_color_fix` +- new lint: `prefer_relative_imports` +- improved messages for `await_only_futures` + +# 0.1.98 + +- fixed null raw expression accesses in `use_to_and_as_if_applicable` +- (internal) migrated to using analyzer `InheritanceManager3` + +# 0.1.97+1 + +- enabled `camel_case_extensions` experimental lint + +# 0.1.97 + +- internal: migrated away from using analyzer `resolutionMap` +- various fixes and improvements to anticipate support for extension-methods +- new lint: `camel_case_extensions` +- rule template generation improvements +- new lint: `avoid_equals_and_hash_code_on_mutable_classes` +- extended `avoid_slow_async_io` to flag async `Directory` methods + +# 0.1.96 + +- fixed false positives in `unnecessary_parens` +- various changes to migrate to preferred analyzer APIs +- rule test fixes + +# 0.1.95 + +- improvements to `unsafe_html` error reporting +- fixed false positive in `prefer_asserts_in_initializer_lists` +- fixed `prefer_const_constructors` to not flag `@literal` annotated constructors + +# 0.1.94 + +- (internal): analyzer API call updates +- (internal): implicit cast cleanup + +# 0.1.93 + +- new lint: `avoid_print` + +# 0.1.92 + +- improved `prefer_collection_literals` to better handle `LinkedHashSet`s and `LinkedHashMap`s +- updates to the Effective Dart rule set +- updated `prefer_final_fields` to be more inclusive +- miscellaneous documentation fixes + +# 0.1.91 + +- fixed missed cases in `prefer_const_constructors` +- fixed `prefer_initializing_formals` to no longer suggest API breaking changes +- updated `omit_local_variable_types` to allow explicit `dynamic`s +- (internal) migration from deprecated analyzer APIs + +# 0.1.90 + +- fixed null-reference in `unrelated_type_equality_checks` +- new lint: `unsafe_html` + +# 0.1.89 + +- broadened `prefer_null_aware_operators` to work beyond local variables +- new lint: `prefer_if_null_operators` +- fixed `prefer_contains` false positives +- fixed `unnecessary_parenthesis` false positives + +# 0.1.88 + +- fixed `prefer_asserts_in_initializer_lists` false positives +- fixed `curly_braces_in_flow_control_structures` to handle more cases +- new lint: `prefer_double_quotes` +- new lint: `sort_child_properties_last` +- fixed `type_annotate_public_apis` false positive for `static const` initializers + +# 0.1.87 + +- change: `prefer_const_constructors_in_immutables` is currently overly permissive, pending analyzer changes (#1537) +- fixed `unnecessary_await_in_return` false positive +- fixed `unrelated_type_equality_checks` false negative with functions +- fixed `prefer_spread_collections` to not lint in const contexts +- fixed false positive in `prefer_iterable_whereType` for `is!` +- fixed false positive in `prefer_collection_literals` for constructors with params + +# 0.1.86 + +- updated `prefer_spread_collections` to ignore calls to `addAll` that could be inlined +- new lint: `prefer_inlined_adds` + +# 0.1.85 + +- (**BREAKING**) renamed `spread_collections` to `prefer_spread_collections` +- new lint: `prefer_for_elements_to_map_fromIterable` +- new lint: `prefer_if_elements_to_conditional_expressions` +- new lint: `diagnostic_describe_all_properties` + +# 0.1.84 + +- new lint: `spread_collections` +- (internal) update to analyzer 0.36.0 APIs +- new lint: `prefer_asserts_with_message` + +# 0.1.83 + +- updated `file_names` to skip prefixed-extension Dart files (e.g., `.css.dart`, `.g.dart`) +- updated SDK constraint to `2.2.0` +- miscellaneous rule documentation fixes +- (internal) updated sources to use Set literals +- fixed NPE in `avoid_shadowing_type_parameters` +- added linter version numbering for use in analyzer summaries +- fixed type utilities to handle inheritance cycles +- (internal) changes to adopt new `package:analyzer` APIs +- fixed `unnecessary_parenthesis` false positives + +# 0.1.82 + +- fixed `prefer_collection_literals` Set literal false positives +- fixed `prefer_const_declarations` Set literal false positives +- new lint: `provide_deprecation_message` + +# 0.1.81 + +- updated `prefer_collection_literals` to support Set literals + +# 0.1.80 + +- deprecated `super_goes_last` +- (internal) migrations to analyzer's preferred `InheritanceManager2` API + +# 0.1.79 + +- `unnecessary_parenthesis` updated to play nicer with cascades +- new lint: `use_full_hex_values_for_flutter_colors` +- new lint: `prefer_null_aware_operators` +- miscellaneous documentation fixes +- removed deprecated lints from the "all options" sample +- stopped registering "default lints" +- `hash_and_equals` fixed to respect `hashCode` fields + +# 0.1.78 + +- restored `prefer_final_locals` to ignore loop variables, and +- introduced a new `prefer_final_in_for_each` lint to handle the `for each` case + +# 0.1.77 + +- updated `prefer_final_locals` to check to for loop variables +- fixed `type_annotate_public_apis` false positives on local functions +- fixed `avoid_shadowing_type_parameters` to report shadowed type parameters in generic typedefs +- fixed `use_setters_to_change_properties` to not wrongly lint overriding methods +- fixed `cascade_invocations` to not lint awaited targets +- fixed `prefer_conditional_assignment` false positives +- fixed `join_return_with_assignment` false positives +- fixed `cascade_invocations` false positives +- miscellaneous documentation improvements +- updated `invariant_booleans` status to experimental + +# 0.1.76 + +- `unnecessary_parenthesis` updated to allow wrapping a `!` argument +- miscellaneous documentation grammar and spelling fixes +- improved error messages for `always_declare_return_types` +- fix `prefer_final_fields ` to work with classes that have generic type arguments +- (internal): deprecated code cleanup +- fixed false positives in `unrelated_type_equality_checks` + +# 0.1.75 + +- analyzer package dependency bumped to `^0.34.0` + +# 0.1.74 + +- experimental lints `avoid_positional_boolean_parameters`, `literal_only_boolean_expressions`, `prefer_foreach`, `prefer_void_to_null` promoted to stable +- `unnecessary_parenthesis` improved to handle function expressions + +# 0.1.73 + +- deprecated `prefer_bool_in_asserts` (redundant w/ Dart 2 checks) +- improved doc generation to highlight deprecated and experimental lints +- bumped analyzer lower-bound to `0.33.4` +- bumped SDK lower-bound to `2.1.0` +- new lint: `unnecessary_await_in_return` + +# 0.1.72 + +- new lint: `use_function_type_syntax_for_parameters` +- internal changes to migrate towards analyzer's new `LinterContext` API +- fix false positive in `use_setters_to_change_properties` +- implementation improvements (and speed-ups) to `prefer_foreach` and `public_member_api_docs` +- new lint: `avoid_returning_null_for_future` +- new lint: `avoid_shadowing_type_parameters` + +# 0.1.71 + +- new lint: `prefer_int_literals` +- update `await_only_futures` to allow awaiting on `null` +- update `use_setters_to_change_properties` to work with `=>` short-hand + +# 0.1.70 + +- fix NPE in `prefer_iterable_whereType` + +# 0.1.69 + +- improved message display for `await_only_futures` +- performance improvements for `null_closures` +- new lint: `avoid_returning_null_for_void` + +# 0.1.68 + +- updated analyzer compatibility to `^0.33.0` + +# 0.1.67 + +- miscellaneous mixin support fixes +- update to `sort_constructors_first` to apply to all members +- update `unnecessary_this` to work on field initializers + +# 0.1.66 + +- broadened SDK version constraint + +# 0.1.65 + +- fix cast exceptions related to mixin support + +# 0.1.64 + +- fixes to better support mixins + +# 0.1.63 + +- updated `unawaited_futures` to ignore assignments within cascades +- new lint: `sort_pub_dependencies` + +# 0.1.62 + +- new lint: `prefer_mixin` +- new lint: `avoid_implementing_value_types` + +# 0.1.61 + +- new lint: `flutter_style_todos` +- improved handling of constant expressions with generic type params +- NPE fix for `invariant_booleans` +- Google lints example moved to `package:pedantic` +- improved docs for `unawaited_futures` + +# 0.1.60 + +- new lint: `avoid_void_async` +- `unawaited_futures` updated to check cascades + +# 0.1.59 + +- relaxed `void_checks` (allowing `T Function()` to be assigned to `void Function()`) +- test and build improvements +- introduced Effective Dart rule set +- Google ruleset updates +- (internal cleanup): move cli main into `lib/` +- fixed false positives in `lines_longer_than_80_chars` +- new lint: `prefer_void_to_null` + +# 0.1.58 + +- roll-back to explicit uses of `new` and `const` to be compatible w/ VMs running `--no-preview-dart-2` + +# 0.1.57 + +- fix to `lines_longer_than_80_chars` to handle CRLF endings +- doc improvements +- set max SDK version to <3.0.0 +- fix to `non_constant_identifier_names` to better handle invalid code +- new lint: `curly_braces_in_flow_control_structures` + +# 0.1.56 + +- fix to `avoid_positional_boolean_parameters` to ignore overridden methods +- fix to `prefer_is_empty` to not evaluate constants beyond int literals +- new lint: `null_closures` +- new lint: `lines_longer_than_80_chars` + +# 0.1.55 + +- fixed an issue in `const` error handling +- updated `linter` binary to use `previewDart2` + +# 0.1.54 + +- new `unnecessary_const` lint +- new `unnecessary_new` lint +- fixed errors in `use_to_and_as_if_applicable` +- new `file_names` lint + +# 0.1.53 + +- updated `unnecessary_statements` to ignore getters (as they may be side-effecting). + +# 0.1.52 + +- fixed `void_checks` to handle arguments not resolved to a parameter +- fixed exceptions produced by `prefer_const_literals_to_create_immutables` + +# 0.1.51 + +- `unrelated_type_equality_checks` now allows comparison between `Int64` or `Int32` and `int` +- `unnecessary_parenthesis` improved to handle cascades _in_ cascades + +# 0.1.50 + +- migration of rules to use analyzer package `NodeLintRule` and `UnitLintRule` yielding significant performance gains all around +- specific performance improvements for `prefer_final_fields` (~6x) +- addressed no such method calls in `void_checks` +- improved lint reporting for various lints + +# 0.1.49 + +- new `void_checks` lint + +# 0.1.48 + +- new `avoid_field_initializers_in_const_classes` lint +- miscellaneous documentation fixes +- improved handling of cascades in `unnecessary_statements` +- new `avoid_js_rounded_ints` lint + +# 0.1.47 + +- new `avoid_double_and_int_checks` lint +- fix to handle uninitialized vars in `prefer_const_declarations` +- fix for generic function type handling in `avoid_types_as_parameter_names` +- new `prefer_iterable_whereType` lint +- new `prefer_generic_function_type_aliases` lint +- Dart 2 compatibility fixes + +# 0.1.46 + +- performance fixes for library prefix testing (`library_prefixes`) +- new `avoid_bool_literals_in_conditional_expressions` lint +- new `prefer_equal_for_default_values` lint +- new `avoid_private_typedef_functions` lint +- new `avoid_single_cascade_in_expression_statements` lint + +# 0.1.45 + +- fix for `invariant_booleans` when analyzing for loops with no condition +- new `avoid_types_as_parameter_names` lint +- new `avoid_renaming_method_parameters` lint + +# 0.1.44 + +- new `avoid_relative_lib_imports` lint +- new `unnecessary_parenthesis` lint +- fix to `prefer_const_literals_to_create_immutables` to handle undefined classes gracefully +- updates to `prefer_const_declarations` to support optional `new` and `const` +- `prefer_const_declarations` updated to check locals +- fixes to `invariant_booleans` +- bumped SDK lower bound to `2.0.0-dev` +- build and workflow improvements: rule template fixes; formatting and header validation +- miscellaneous documentation fixes + +# 0.1.43 + +- new `prefer_const_declarations.dart` lint +- new `prefer_const_literals_to_create_immutables` lint +- miscellaneous documentation improvements + +# 0.1.42 + +- added support for external constructors in `avoid_unused_constructor_parameters` +- added code reference resolution docs for `comment_references` + +# 0.1.41 + +- broadened `args` package dependency to support versions `1.*` + +# 0.1.40 + +- `avoid_unused_constructor_parameters` updated to better handle redirecting factory constructors +- `avoid_returning_this` improvements +- `prefer_bool_in_asserts` improvements +- miscellaneous documentation fixes + +# 0.1.39 + +- `prefer_interpolation_to_compose_strings` updated to allow concatenation of two non-literal strings +- `prefer_interpolation_to_compose_strings` updated to allow `+=` +- lots of rule documentation fixes and enhancements +- fix for `prefer_const_constructors_in_immutables` false positive with redirecting factory constructors + +# 0.1.38 + +- `public_member_api_docs` fix for package URIs + +# 0.1.37 + +- `avoid_positional_boolean_parameters` updated to allow booleans in operator declarations +- `comment_references` fixed to handle incomplete references +- `non_constant_identifier_names` updated to allow underscores around numbers + +# 0.1.36 + +- new `avoid_unused_constructor_parameters` lint +- new `prefer_bool_in_asserts` lint +- new `prefer_typing_uninitialized_variables` lint +- new `unnecessary_statements` lint +- `public_member_api_docs` updated to only lint source in `lib/` +- 'avoid_empty_else' fixed to ignore synthetic `EmptyStatement`s +- updated library prefix checking to allow leading `$`s +- miscellaneous documentation fixes +- Dart SDK constraints restored (removed unneeded `2.0.0-dev.infinity` constraint) + +# 0.1.35 + +- linter engine updated to use new analysis driver + +# 0.1.34 + +## Features + +- `non_constant_identifier_names` extended to include named constructors +- SDK constraint broadened to `2.0.0-dev.infinity` +- improved `prefer_final_fields` performance + +## Fixes + +- fixes to `unnecessary_overrides` (`noSuchMethod` handling, return type narrowing, special casing of documented `super` calls) +- fix to `non_constant_identifier_names` to handle identifiers with no name +- fixes to `prefer_const_constructors` to support list literals +- fixes to `recursive_getters` +- fixes to `cascade_invocations` + +# 0.1.33 + +## Features + +- new `prefer_const_constructors_in_immutables` lint +- new `always_put_required_named_parameters_first` lint +- new `prefer_asserts_in_initializer_lists` lint +- support for running in `--benchmark` mode +- new `prefer_single_quote_strings` lint + +## Fixes + +- docs for `avoid_setters_without_getters` +- fix to `directives_ordering` to work with part directives located after exports +- fixes to `cascade_invocations` false positives +- fixes to `literal_only_boolean_expressions` false positives +- fix to ensure `cascade_invocations` only lints method invocations if target is a simple identifier +- fixes to `use_string_buffers` false positives +- fixes to `prefer_const_constructors` + +# 0.1.32 + +- Lint stats (`-s`) output now sorted. + +# 0.1.31 + +- New `prefer_foreach` lint. +- New `use_string_buffers` rule. +- New `unnecessary_overrides` rule. +- New `join_return_with_assignment_when_possible` rule. +- New `use_to_and_as_if_applicable` rule. +- New `avoid_setters_without_getters` rule. +- New `always_put_control_body_on_new_line` rule. +- New `avoid_positional_boolean_parameters` rule. +- New `always_require_non_null_named_parameters` rule. +- New `prefer_conditional_assignment` rule. +- New `avoid_types_on_closure_parameters` rule. +- New `always_put_control_body_on_new_line` rule. +- New `use_setters_to_change_properties` rule. +- New `avoid_returning_this` rule. +- New `avoid_annotating_with_dynamic_when_not_required` rule. +- New `prefer_constructors_over_static_methods` rule. +- New `avoid_returning_null` rule. +- New `avoid_classes_with_only_static_members` rule. +- New `avoid_null_checks_in_equality_operators` rule. +- New `avoid_catches_without_on_clauses` rule. +- New `avoid_catching_errors` rule. +- New `use_rethrow_when_possible` rule. +- Many lint fixes (notably `prefer_final_fields`, `unnecessary_lambdas`, `await_only_futures`, `cascade_invocations`, `avoid_types_on_closure_parameters`, and `overridden_fields`). +- Significant performance improvements for `prefer_interpolation_to_compose_strings`. +- New `unnecessary_this` rule. +- New `prefer_initializing_formals` rule. + +# 0.1.30 + +- New `avoid_function_literals_in_foreach_calls` lint. +- New `avoid_slow_async_io` lint. +- New `cascade_invocations` lint. +- New `directives_ordering` lint. +- New `no_adjacent_strings_in_list` lint. +- New `no_duplicate_case_values` lint. +- New `omit_local_variable_types` lint. +- New `prefer_adjacent_string_concatenation` lint. +- New `prefer_collection_literals` lint. +- New `prefer_const_constructors` lint. +- New `prefer_contains` lint. +- New `prefer_expression_function_bodies` lint. +- New `prefer_function_declarations_over_variables` lint. +- New `prefer_initializing_formals` lint. +- New `prefer_interpolation_to_compose_strings` lint. +- New `prefer_is_empty` lint. +- New `recursive_getters` lint. +- New `unnecessary_brace_in_string_interps` lint. +- New `unnecessary_lambdas` lint. +- New `unnecessary_null_aware_assignments` lint. +- New `unnecessary_null_in_if_null_operators` lint. +- Miscellaneous bug fixes and codegen improvements. + +# 0.1.29 + +- New `cascade_invocations` lint. +- Expand `await_only_futures` to accept classes that extend or implement `Future`. +- Improve camel case regular expression tests to accept `$`s. +- Fixes to `parameter_assignments` (improved getter handling and an NPE). + +# 0.1.27 + +- Fixed cast exception in `dart_type_utilities` (dart-lang/sdk#27405). +- New `parameter_assignments` lint. +- New `prefer_final_fields` lint. +- New `prefer_final_locals` lint. +- Markdown link fixes in docs (#306). +- Miscellaneous solo test running fixes and introduction of `solo_debug` (#304). + +# 0.1.26 + +- Updated tests to use package `test` (#302). + +# 0.1.25 + +- Fixed false positive on `[]=` in `always_declare_return_types` (#300). +- New `invariant_booleans` lint. +- New `literal_only_boolean_expressions` lint. +- Fixed `camel_case_types` to allow `$` in identifiers (#290). + +# 0.1.24 + +- Internal updates to keep up with changes in the analyzer package. +- Updated `close_sinks` to respect calls to `destroy` (#282). +- Fixed `only_throw_errors` to report on the expression not node. + +# 0.1.23 + +- Removed `whitespace_around_ops` pending re-name and re-design (#249). + +# 0.1.22 + +- Grinder support (`rule:rule_name` and `docs:location`) for rule stub and doc generation (respectively). +- Fix to allow leading underscores in `non_constant_identifier_names`. +- New `valid_regexps` lint (#277). +- New `whitespace_around_ops` lint (#249). +- Fix to `overridden_fields` to flag overridden static fields (#274). +- New `list_remove_unrelated_type` to detect passing a non-`T` value to `List.remove()`` (#271). +- New `empty_catches` lint to catch empty catch blocks (#43). +- Fixed `close_sinks` false positive (#268). +- `linter` support for `--strong` to allow for running linter in strong mode. + +# 0.1.21 + +- New `only_throw_errors` lint. +- New lint to check for `empty_statements` (#259). +- Fixed NSME when file contents cannot be read (#260). +- Fixed unsafe cast in `iterable_contains_unrelated_type` (#267). + +# 0.1.20 + +- New `cancel_subscriptions` lint. + +# 0.1.19 + +- New `close_sinks` lint. +- Fixes to `iterable_contains_unrelated_type `. + +# 0.1.18 + +- Fix NSME in `iterable_contains_unrelated_type` (#245). +- Fixed typo in `comment_references` error description. +- Fix `overriden_field` false positive (#246). +- Rename linter binary `lints` option to `rules` (#248). +- Help doc tweaks. + +# 0.1.17 + +- Fix to `public_member_api_docs` to check for documented getters when checking setters (#237). +- New `iterable_contains_unrelated_type` lint to detect when `Iterable.contains` is invoked with an object of an unrelated type. +- New `comment_references` lint to ensure identifiers referenced in docs are in scope (#240). + +# 0.1.16 + +- Fix for false positive in `overriden_field`s. +- New `unrelated_type_equality_checks` lint. +- Fix to accept `$` identifiers in string interpolation lint (#214). +- Update to new `plugin` API (`0.2.0`). +- Strong mode cleanup. + +# 0.1.15 + +- Fix to allow simple getter/setters when a decl is ``@protected` (#215). +- Fix to not require type params in `is` checks (#227). +- Fix to not flag field formal identifiers in parameters (#224). +- Fix to respect filters when calculating error codes (#198). +- Fix to allow `const` and `final` vars to be initialized to null (#210). +- Fix to respect commented blocks in `empty_constructor_bodies` (#209). +- Fix to check types on list/map literals (#199). +- Fix to skip `main` when checking for API docs (#207). +- Fix to allow leading `$` in type names (#220). +- Fix to ignore private typedefs when checking for types (#216). +- New `test_types_in_equals` lint. +- New `await_only_futures` lint. +- New `throw_in_finally` lint. +- New `control_flow_in_finally` lint. + +# 0.1.14 + +- Fix to respect `@optionalTypeArgs` (#196). +- Lint to warn if a field overrides or hides other field. +- Fix to allow single char UPPER_CASE non-constants (#201). +- Fix to accept casts to dynamic (#195). + +# 0.1.13 + +- Fix to skip overriding members in API doc checks (`public_member_api_docs`). +- Fix to suppress lints on synthetic nodes/tokens (#193). +- Message fixes (`annotate_overrides`, `public_member_api_docs`). +- Fix to exclude setters from return type checks (#192). + +# 0.1.12 + +- Fix to address `LibraryNames` regexp that in pathological cases went exponential. + +# 0.1.11 + +- Doc generation improvements (now with options samples). +- Lint to sort unnamed constructors first (#187). +- Lint to ensure public members have API docs (#188). +- Lint to ensure constructors are sorted first (#186). +- Lint for `hashCode` and `==` (#168). +- Lint to detect un-annotated overrides (#167). +- Fix to ignore underscores in public APIs (#153). +- Lint to check for return types on setters (#122). +- Lint to flag missing type params (#156). +- Lint to avoid inits to `null` (#160). + +# 0.1.10 + +- Updated to use `analyzer` `0.27.0`. +- Updated options processing to handle untyped maps (dart-lang/sdk#25126). + +# 0.1.9 + +- Fix `type_annotate_public_apis` to properly handle getters/setters (#151; dart-lang/sdk#25092). + +# 0.1.8 + +- Fix to protect against errors in linting invalid source (dart-lang/sdk#24910). +- Added `avoid_empty_else` lint rule (dart-lang/sdk#224936). + +# 0.1.7 + +- Fix to `package_api_docs` (dart-lang/sdk#24947; #154). + +# 0.1.6 + +- Fix to `package_prefixed_library_names` (dart-lang/sdk#24947; #154). + +# 0.1.5 + +- Added `prefer_is_not_empty` lint rule (#143). +- Added `type_annotate_public_apis` lint rule (#24). +- Added `avoid_as` lint rule (#145). +- Fixed `non_constant_identifier_names` rule to special case underscore identifiers in callbacks. +- Fix to escape `_`s in callback type validation (addresses false positives in `always_specify_types`) (#147). + +# 0.1.4 + +- Added `always_declare_return_types` lint rule (#146). +- Improved `always_specify_types` to detect missing types in declared identifiers and narrowed source range to the token. +- Added `implementation_imports` lint rule (#33). +- Test performance improvements. + +# 0.1.3+5 + +- Added `always_specify_types` lint rule (#144). + +# 0.1.3+4 + +- Fixed linter registry memory leaks. + +# 0.1.3 + +- Fixed various options file parsing issues. + +# 0.1.2 + +- Fixed false positives in `unnecessary_brace_in_string_interp` lint. Fix #112. + +# 0.1.1 + +- Internal code and dependency constraint cleanup. + +# 0.1.0 + +- Initial stable release. + +# 0.0.2+1 + +- Added machine output option. Fix #69. +- Fixed resolution of files in `lib/` to use a `package:` URI. Fix #49. +- Tightened up `analyzer` package constraints. +- Fixed false positives in `one_member_abstracts` lint. Fix #64. + +# 0.0.2 + +- Initial push to pub. diff --git a/pkg/linter/CONTRIBUTING.md b/pkg/linter/CONTRIBUTING.md new file mode 100644 index 00000000000..32ed6810a2c --- /dev/null +++ b/pkg/linter/CONTRIBUTING.md @@ -0,0 +1,87 @@ +Contributing to the Linter +========================== + +[![Build Status](https://github.com/dart-lang/linter/workflows/linter/badge.svg)](https://github.com/dart-lang/linter/actions) +![GitHub contributors](https://img.shields.io/github/contributors/dart-lang/linter.svg) + + +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +_See also: [Dart's code of conduct](https://dart.dev/code-of-conduct)_ + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. + +### File headers +All files in the project must start with the following header. + + // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file + // for details. All rights reserved. Use of this source code is governed by a + // BSD-style license that can be found in the LICENSE file. + +### Mechanics +Contributing code is easy. + + * First, get the source by forking `https://github.com/dart-lang/linter` into your own GitHub account. + * If you haven't configured your machine with an SSH key that's known to GitHub then follow the directions here: https://help.github.com/articles/generating-ssh-keys/. + * `git clone git@github.com:/linter.git` + * `cd linter` + * `git remote add upstream git@github.com:dart-lang/linter.git` (So that you fetch from the main repository, not your clone, when running git fetch et al.) + +To start working on a patch: + + * `git fetch upstream` + * `git checkout upstream/main -b name_of_your_branch` + * Hack away. (Before committing, please be sure and run `dart format` on modified files; our build will fail if you don't!) + * `git commit -a -m ""` + * `git push origin name_of_your_branch` + +To send us a pull request: + + * `git pull-request` (if you are using [Hub](https://github.com/github/hub/)) or + go to `https://github.com/dart-lang/linter` and click the + "Compare & pull request" button + * either explicitly name a reviewer in the GitHub UI or add their GitHub name in the pull request message body + +Please make sure all your checkins have detailed commit messages explaining the patch and if a PR is *not* ready to land, consider making it clear in the description and/or prefixing the title with "WIP". + +Please note that a few kinds of changes require a `CHANGELOG` entry. Notably, any change that: + +1. adds a new lint +2. removes or deprecates a lint or +3. fundamentally changes the semantics of an existing lint + +should have a short entry in the `CHANGELOG`. Feel free to bring up any questions in your PR. + +Once you've gotten an LGTM from a project maintainer, submit your changes to the +`main` branch using one of the following methods: + +* Wait for one of the project maintainers to submit it for you. +* Click the green "Merge pull request" button on the GitHub UI of your pull + request (requires commit access) +* `git push upstream name_of_your_branch:main` (requires commit access) +* Having done all this, please make sure we have a good email address so we can credit you in our `AUTHORS` file. +* Thank you! + +### The small print +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). diff --git a/pkg/linter/LICENSE b/pkg/linter/LICENSE new file mode 100644 index 00000000000..633672ab360 --- /dev/null +++ b/pkg/linter/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/linter/README.md b/pkg/linter/README.md new file mode 100644 index 00000000000..7d33ca53e63 --- /dev/null +++ b/pkg/linter/README.md @@ -0,0 +1,99 @@ +# Linter for Dart + +The Dart Linter package defines lint rules that identify and report on "lints" found in Dart code. Linting is performed by the Dart +analysis server and the `dart analyze` command in the [Dart command-line tool][dart_cli]. + +[![Build Status](https://github.com/dart-lang/linter/workflows/linter/badge.svg)](https://github.com/dart-lang/linter/actions) +[![Coverage Status](https://coveralls.io/repos/dart-lang/linter/badge.svg)](https://coveralls.io/r/dart-lang/linter) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/dart-lang/linter/badge)](https://deps.dev/project/github/dart-lang%2Flinter) + +## Installing + +The linter is bundled with the Dart [SDK](https://dart.dev/tools/sdk); if you have an updated Dart SDK already, you're done! + +Alternatively, if you want to contribute to the linter or examine the source, clone the `linter` repo like this: + + $ git clone https://github.com/dart-lang/linter.git + +## Usage + +The linter gives you feedback to help you catch potential errors and keep your code in line with the published +[Dart Style Guide][style_guide]. Enforceable lint rules (or "lints") are cataloged [here][lints] and can be configured via an +[analysis options file][options_file]. The linter is run from within the `dart analyze` [command-line tool][analyzer_cli] shipped with the +Dart SDK. Assuming you have lints configured in an `analysis_options.yaml` file at the root of your project with these contents: + +```yaml +linter: + rules: + - annotate_overrides + - hash_and_equals + - prefer_is_not_empty +``` +you could lint your package like this: + + $ dart analyze . + +and see any violations of the `annotate_overrides`, `hash_and_equals`, and `prefer_is_not_empty` rules in the console. +To help you choose the rules you want to enable for your package, we have provided a [complete list of rules][lints] +with lints recommended by the Dart team collected in [`package:lints`][package-dart-lints]. Lints recommended for Flutter apps, packages, +and plugins are documented in [`package:flutter_lints`][package-flutter-lints]. + +If a specific lint warning should be ignored, it can be flagged with a comment. For example, + +```dart + // ignore: camel_case_types + class whyOhWhy { } +``` + +tells the Dart analyzer to ignore this instance of the `camel_case_types` warning. + +End-of-line comments are supported as well. The following communicates the same thing: + +```dart + class whyOhWhy { // ignore: camel_case_types +``` + +To ignore a rule for an entire file, use the `ignore_for_file` comment flag. For example, + +```dart +// ignore_for_file: camel_case_types + +... + +class whyOhWhy { } +``` + +tells the Dart analyzer to ignore all occurrences of the `camel_case_types` warning in this file. + +As lints are treated the same as errors and warnings by the analyzer, their severity can similarly be configured in an options file. For +example, an analysis options file that specifies + +```yaml +linter: + rules: + - camel_case_types +analyzer: + errors: + camel_case_types: error +``` + +tells the analyzer to treat `camel_case_types` lints as errors. For more on configuring analysis see the analysis option file [docs][options_file]. + +## Contributing + +Feedback is greatly appreciated and contributions are welcome! Please read the +[contribution guidelines](CONTRIBUTING.md); mechanics of writing lints are covered [here](doc/writing-lints.md). + +## Features and bugs + +Please file feature requests and bugs in the [issue tracker][tracker]. + +[analyzer_cli]: https://dart.dev/tools/dart-analyze +[dart_cli]: https://dart.dev/tools/dart-tool +[effective_dart]: https://dart.dev/effective-dart +[lints]: https://dart.dev/lints +[options_file]: https://dart.dev/guides/language/analysis-options#the-analysis-options-file +[package-dart-lints]: https://github.com/dart-lang/lints +[package-flutter-lints]: https://github.com/flutter/packages/tree/main/packages/flutter_lints +[style_guide]: https://dart.dev/effective-dart/style +[tracker]: https://github.com/dart-lang/linter/issues diff --git a/pkg/linter/analysis_options.yaml b/pkg/linter/analysis_options.yaml new file mode 100644 index 00000000000..7efc66a1bf4 --- /dev/null +++ b/pkg/linter/analysis_options.yaml @@ -0,0 +1,67 @@ +include: package:lints/recommended.yaml + +analyzer: + exclude: + # ignore vscode .history files + - .history/** + - test_data/** + language: + strict-casts: true +linter: + rules: + - always_put_required_named_parameters_first + - avoid_annotating_with_dynamic + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_dynamic_calls + - avoid_positional_boolean_parameters + - avoid_print + - avoid_redundant_argument_values + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_this + - avoid_setters_without_getters + - avoid_slow_async_io + - avoid_unused_constructor_parameters + - cancel_subscriptions + - cast_nullable_to_non_nullable + - collection_methods_unrelated_type + - comment_references + - dangling_library_doc_comments + - directives_ordering + - discarded_futures + - implicit_call_tearoffs + - join_return_with_assignment + - library_annotations + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - noop_primitive_operations + - only_throw_errors + - package_api_docs + - parameter_assignments + - prefer_asserts_in_initializer_lists + - prefer_const_constructors_in_immutables + - prefer_constructors_over_static_methods + - prefer_expression_function_bodies + - prefer_foreach + - prefer_null_aware_method_calls + - prefer_relative_imports + - prefer_single_quotes + - sort_pub_dependencies + - test_types_in_equals + - throw_in_finally + - unawaited_futures # pedantic + - unnecessary_breaks + - unnecessary_final + - unnecessary_lambdas + - unnecessary_library_directive + - unnecessary_null_checks + - unnecessary_parenthesis + - unnecessary_statements + - unreachable_from_main + - use_if_null_to_convert_nulls_to_bools + - use_late_for_private_fields_and_variables + - use_setters_to_change_properties + - use_string_buffers + - use_super_parameters + - use_to_and_as_if_applicable diff --git a/pkg/linter/bin/linter.dart b/pkg/linter/bin/linter.dart new file mode 100644 index 00000000000..d6fd57b230d --- /dev/null +++ b/pkg/linter/bin/linter.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/cli.dart' as cli; + +Future main(List args) async { + await cli.run(args); +} diff --git a/pkg/linter/doc/lint-lifecycle.md b/pkg/linter/doc/lint-lifecycle.md new file mode 100644 index 00000000000..3fc7a5e7adb --- /dev/null +++ b/pkg/linter/doc/lint-lifecycle.md @@ -0,0 +1,79 @@ +# The Life of a Lint + +## Lint States + +### Proposed + +A lint begins its life as a **proposal** which is _pending_ until _accepted_. + +### Accepted + +After discussion and sufficient agreement within the team, a proposed lint becomes **accepted**. + +An accepted lint is ready for implementation. + +### Experimental + +Lints can be marked experimental. + +Reasons a lint might be marked experimental include a lint + +* being tentatively introduced +* having unknown value +* having incomplete semantics +* having outstanding (fixable) false positives or +* that might be known to be temporary. + +### Stable + +Stable lints are lints whose semantics and implementation are considered complete. False +positives (that are not known, accepted and documented limitations of lint semantics) are +considered bugs and fixing them should be prioritized. False negatives may be bugs or +enhancements (depending on semantics). + +### Deprecated + +Deprecated lints are lints that we plan to remove. + +Reasons for deprecation include: + +* semantics that don't make sense with current language semantics (e.g., null-safety) +* stale advice +* poor performance or +* poor user experience (e.g., too many false positives). + +### Removed + +Lints we no longer support are removed. + +In general removal is preceded by a period of deprecation. + +## State Transitions + +### Implementation + +Once a lint proposal is accepted, it is safe to implement. A change that lands a new lint should +have a corresponding `CHANGELOG` entry. + +### Deprecation + +Implemented lints can be deprecated. + +Deprecating lints that are in common lint sets (e.g., in [package:lints](https://github.com/dart-lang/lints) +can be impactful so should be done with care. + +(**NOTE**: A change that deprecates an existing lint should have a corresponding `CHANGELOG` entry.) + +### Marking Stable + +Experimental lints should aspire to be stable. An experimental lint is a candidate for stable when it has + +* complete semantics +* complete implementation (with no false-positives) +* established long-term value (e.g., inclusion in a recommended lint set) + +### Removal + +Deprecated or experimental lints can be removed. A stable lint should be deprecated before removal. + +(**NOTE**: A change that removes an existing lint should have a corresponding `CHANGELOG` entry.) diff --git a/pkg/linter/doc/use_build_context_synchronously.md b/pkg/linter/doc/use_build_context_synchronously.md new file mode 100644 index 00000000000..ddcbbb35322 --- /dev/null +++ b/pkg/linter/doc/use_build_context_synchronously.md @@ -0,0 +1,119 @@ +# `use_build_context_synchronously` design + +## Background + +The idea behind the `use_build_context_synchronously` lint rule requires +careful tracking of a function body's possible control flow. At various points +in the syntax tree, we must be able to answer questions like, "when this +expression is reached, is it _possible_ that we have traversed through an +asynchronous gap?" and "does this mounted check _definitely_ guard this +expression?" The intricacies of such tracking are quite different from the +requirements of most linter rules. + +## High level design + +At a high level, the task of the lint rule can be broken down into three steps: + +1. Consider each expression which references a BuildContext value, and which + may be vulnerable to async gap bugs. For each such expression: +2. Walk up the syntax tree from the expression, until reaching the boundary of a + function body (don't walk outside an anonymous function body, or a method + declaration, etc.). At each ancestor node (well, not exactly ancestor, see + below): +3. Compute the "async state" between the ancestor node and the expression-in- + question, by visiting the ancestor's descendent nodes, looking for await + expressions and mounted checks. + +The first step just uses the linter's standard rule-registering mechanism. The +latter steps are explained below. + +### Mounted guards + +It would be egregious to require a function with a reference to a BuildContext +to have _zero_ async gaps (await expressions) above the reference. Developers +are allowed a safeguard: a "mounted check" which leads to a "mounted guard." + +* A "mounted check" is merely an expression in which the `mounted` property of a + BuildContext expression is read. It typically looks like `context.mounted`. +* A mounted check leads to a "mounted guard" for a certain node if it guarantees + that the node will only be executed if the BuildContext is mounted, that is, + the mounted check has returned positive. + +## Walking up the ancestors and their preceding siblings + +In this step, we have a single expression with a **reference** to a BuildContext +object, and we need to examine nodes which contain an async gap _which could be +crossed before the access to the **reference**_. This means we are simulating, +to a very limited extent, the runtime flow between an async gap and +**reference**. + +1. Start with the expression containing the **reference**. +2. Define a **child** node whose value is initially **reference**. +3. While **child** is not a function body: + a. Set a **parent** node equal to the child's parent. + b. Check the "async state" between **parent** and **child**. + c. If the state is "asynchronous," then there is a _possible_ async gap + between **parent** and **child**, so report a lint. + d. If the state is "mounted guard," then there is a _definite_ mounted + guard between **parent** and **child**, so **child** is safe, and we can + stop checking **child**. + +Note: We use **child** in the loop, rather than just **reference**, in order to +make use of **child**'s relationship to **parent**, when computing the "async +gap" (see the next section). + +Given the way we walk up the syntax tree, the async state between **parent** and +**child** is the same state as between **parent** and **reference**: if there is +a _possible_ async gap between **parent** and **child**, then there is a +possible async gap between **parent** and **reference**, and if there is a +_definite_ mounted guard between **parent** and **child**, then there is a +_definite_ mounted guard between **parent** and **reference**. + +## Computing the "async state" of one node relative to another + +This is the most complex and delicate step. Given two nodes, a **parent** and +**child**, we must calculate whether there is a _possible_ async gap between the +two (with no mounted guard between the async gap and the **child**), or a +_definite_ mounted guard between the two (without a possible async gap between +the mounted guard and **child**), or no interesting async state between the two. +This calculation is based on a few simple properties: + +* We implement this calculation with a standard SimpleVisitor from the analyzer. + We create the visitor with **child** as the reference node to consider. Each + visit method descends down various child nodes, receiving their returned + "async state" in order to calculate the state of it's own visited node, + relative to **child**. +* At the entrypoint of the visitation, **child**'s parent is **parent**. Then we + descend into **parent**'s various descendents, and so for every other visit + method, **child** is a sibling of the visited node (in a NodeList), or is a + child of some ancestor of the visited node. +* For nodes with multiple children, the associated visit method must take the + position of **child** into account, as an async gap only affects **child** if + it occurs _before_ **child**. Examples: + * YieldStatement - this node has one child. If **child** is that child, then + the async state is "uninteresting" (`null`). Otherwise, **child** follows + the YieldStatement, and any await expressions occurring in the + YieldStatement's expression result in `AsyncState.asynchronous`. + * Block - this node has a list of child Statements. If **child** is one of + those, then we compute the async state between each Statement that precedes + **child** and **child**. We do not consider the Statements that follow + **child** (unless the parent of the Block is a DoStatement, ForStatement, or + WhileStatement). We consider each preceding Statement in reverse order, + starting with the Statement that immediately precedes **child**. In this way + we can correctly identify that a Statement which acts as a mounted guard for + **child**. If **child** is not one of the Statements, then it follows the + Block, and any await expressions occurring in the Block's expressions result + in `AsyncState.asynchronous`. + * MethodInvocation - this node has a target Expression and an argument list of + Expressions. At runtime, the target is evaluated before the argument list, + and the arguments are evaluated in left-to-right order. The associated visit + method uses these facts to compute, for example, that if **child** is the + target, then any async gaps in the argument list do not affect it. +* For nodes which can affect control flow (IfStatement, IfElement, + ConditionalExpression, etc.), the associated visit methods must take + **child**'s position into account for the purposes of mounted guards. For + example, an IfStatement with a condition like `context.mounted` guards the + then-statement, but not the else-statement, and no statements that follow the + IfStatement. An IfStatement with a condition like `!context.mounted` and a + then-statement that definitely exits (e.g. with a return or a throw), + definitely guards the statements that follow it. \ No newline at end of file diff --git a/pkg/linter/doc/writing-lints.md b/pkg/linter/doc/writing-lints.md new file mode 100644 index 00000000000..8e2f28fc09f --- /dev/null +++ b/pkg/linter/doc/writing-lints.md @@ -0,0 +1,169 @@ +# Writing Lints + +Preliminary notes on writing lints. + +## Lint Criteria + +Borrowing heavily from the criteria for [adding new checks to errorprone], +lints should have the following properties. + +Dart lints: + +* should be easy to understand. The problem should be obvious once the linter + points it out. +* should have a correspondingly easy fix. For example, "Remove this type + annotation", or "Delete these braces", not "Introduce a new subclass and + override methods A, B, and C." +* should have *few* false positives. + +## Lint Properties + +Every lint has a: + +**Name.** A short name using [Dart package naming conventions]. Naming is +*hard* but strive to be concise and consistent. Prefer to use the _problem_ as +the name, as in the existing lints `control_flow_in_finally` and +`empty_catches`. Do not start a lint's name with "always", "avoid", or +"prefer". Where possible, use existing rules for inspiration and observe the +rules of [parallel construction]. + +**Description.** A short description of the lint, suitable for printing in +console output. For example: + +``` +[lint] DO name types using UpperCamelCase. +``` + +**Kind.** The first word in the description should identify the *kind* of lint +where kinds are derived from the [style guide]. In summary: + +* ***DO*** guidelines describe practices that should always be followed. There +will almost never be a valid reason to stray from them. + +* ***DON'T*** guidelines are the converse: things that are almost never a good +idea. You'll note there are few of these here. Guidelines like these in other +languages help to avoid the pitfalls that appear over time. Dart is new enough +that we can just fix those pitfalls directly instead of putting up ropes around +them. + +* ***PREFER*** guidelines are practices that you should follow. However, there +may be circumstances where it makes sense to do otherwise. Just make sure you +understand the full implications of ignoring the guideline when you do. + +* ***AVOID*** guidelines are the dual to "prefer": stuff you shouldn't do but +where there may be good reasons to on rare occasions. + +* ***CONSIDER*** guidelines are practices that you might or might not want to +follow, depending on circumstances, precedents, and your own preference. + +**Detailed Description.** In addition to the short description, a lint rule +should have more detailed rationale with code examples, ideally *good* and +*bad*. The [style guide] is a great source for inspiration. Many style +recommendations have been directly translated to lints as enumerated +[here][lints]. + +**Group.** A grouping. For example, *Style Guide* aggregates style guide +derived lints. + +**Maturity.** Rules can be further distinguished by maturity. Unqualified rules +are considered stable, while others may be marked *EXPERIMENTAL* or *PROPOSED* +to indicate that they are under review. + +## Mechanics + +Lints live in the [lib/src/rules] directory. Corresponding unit tests live in +[test/rules]. + +Rule stubs can be generated with the [rule.dart] helper script: + + $ dart tool/rule.dart -n my_new_lint + +generates lint and test stubs in `lib/src/rules` and `test/rules`. + +### Analyzer APIs + +The linter has a close relationship with the `analyzer` package and at times +reaches into non-public APIs. For the most part, we have isolated these +references in an [analyzer.dart utility library]. *Wherever possible please +use this library to access analyzer internals.* + + * If `analyzer.dart` is missing something please consider opening an issue + where we can discuss how best to add it. + * If you find yourself tempted to make references to analyzer + [implementation classes][implementation_imports] also consider opening an + issue so that we can see how best to manage the new dependency. + +Thanks! + +### Dart Language Specification + +When writing lints, it can be useful to have the [Dart Language Specification] +handy. If you're working to support bleeding edge language features, you'll +want the [latest draft][draft language spec]. + +### Writing Tests that Depend on Dart SDK Details + +**Important:** when writing tests that use standard `dart:` libraries, it's +important to keep in mind that linter tests use a mocked SDK that has only a +small subset of the real one. We do this for performance reasons as it's FAR +faster to load a mock SDK into memory than read the real one from disk. If you +are writing tests that depend on something in the Dart SDK (for example, an +interface such as `Iterable`), you may need to update SDK mock content located +in the `package:analyzer` [test utilities `mock_sdk.dart`][mock_sdk.dart]. + +### Running Tests + +The test suite run during the linter's CI, can be run locally like so: + + $ dart test/all.dart + +alternatively, tests can be run using `pub`: + + $ dart test + +Running a single test can be done using the `rule_debug` helper: + + $ dart test/util/rule_debug.dart always_declare_return_types + +would only test `always_declare_return_types`. (This can be very handy if you +want to run tests in the VM debugger). + +If you simply want to verify a test, you can run it solo in `pub`: + + $ dart test -N always_declare_return_types + +### Utilities + +You'll notice when authoring a new rule that failures cause the AST of the test +case to be displayed to `stdout`. If you simply want to dump the AST of a given +compilation unit, you can use the `spelunk` helper directly. For example: + + $ dart tool/spelunk.dart lib/src/rules.dart + +would dump the AST of `rules.dart`. + +### Performance + +For performance reasons rules should prefer implementing `NodeLintRule` and +registering interest in specific AST node types using +`registry.addXYZ(this, visitor)`. Avoid overriding `visitCompilationUnit()` and +performing your own full `CompilationUnit` visits. + +# Feedback is Welcome! + +Details are under active development. Feedback is most [welcome][issues]! + +[adding new checks to errorprone]: https://github.com/google/error-prone/wiki/Criteria-for-new-checks +[Dart package naming conventions]: https://dart.dev/tools/pub/pubspec#name +[parallel construction]: https://en.wikipedia.org/wiki/Parallelism_(grammar) +[style guide]: https://dart.dev/effective-dart/style/ +[lints]: https://dart.dev/lints +[lib/src/rules]: https://github.com/dart-lang/linter/tree/main/lib/src/rules +[test_data/rules]: https://github.com/dart-lang/linter/tree/main/test_data/rules +[rule.dart]: https://github.com/dart-lang/linter/blob/main/tool/rule.dart +[analyzer.dart utility library]: https://github.com/dart-lang/linter/blob/main/lib/src/analyzer.dart +[implementation_imports]: https://dart.dev/lints/implementation_imports +[Dart Language Specification]: https://dart.dev/guides/language/spec +[draft language spec]: https://spec.dart.dev/DartLangSpecDraft.pdf +[mock_sdk.dart]: https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart +[issues]: https://github.com/dart-lang/linter/issues diff --git a/pkg/linter/example/README.md b/pkg/linter/example/README.md new file mode 100644 index 00000000000..d728e7af8e6 --- /dev/null +++ b/pkg/linter/example/README.md @@ -0,0 +1,14 @@ +# Examples + +Some example analysis options files. + +* [all.yaml](all.yaml) - auto-generated options enabling all lints. + +For more on lint rule sets see the analyzer docs on +[customizing static analysis](https://dart.dev/guides/language/analysis-options). + +Official Dart lint rules are maintained in +[`package:lints`](https://github.com/dart-lang/lints/). + +Flutter team rules are maintained in +[`package:flutter_lints`](https://github.com/flutter/packages/tree/main/packages/flutter_lints). diff --git a/pkg/linter/example/all.yaml b/pkg/linter/example/all.yaml new file mode 100644 index 00000000000..63510240ea3 --- /dev/null +++ b/pkg/linter/example/all.yaml @@ -0,0 +1,220 @@ +# Auto-generated options enabling all lints. +# Add these to your `analysis_options.yaml` file and tailor to fit! +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_specify_types + - always_use_package_imports + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks diff --git a/pkg/linter/lib/src/analyzer.dart b/pkg/linter/lib/src/analyzer.dart new file mode 100644 index 00000000000..af85fe2e0f5 --- /dev/null +++ b/pkg/linter/lib/src/analyzer.dart @@ -0,0 +1,177 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: implementation_imports + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/src/generated/engine.dart' show AnalysisErrorInfo; +import 'package:analyzer/src/lint/io.dart' show errorSink; +import 'package:analyzer/src/lint/linter.dart'; +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer/src/lint/util.dart' as util; +import 'package:analyzer/src/services/lint.dart' as lint_service; +import 'package:pub_semver/pub_semver.dart'; + +export 'package:analyzer/dart/element/type_system.dart'; +export 'package:analyzer/src/dart/ast/token.dart'; +export 'package:analyzer/src/dart/element/inheritance_manager3.dart' + show InheritanceManager3, Name; +export 'package:analyzer/src/dart/error/lint_codes.dart'; +export 'package:analyzer/src/dart/resolver/exit_detector.dart'; +export 'package:analyzer/src/generated/engine.dart' show AnalysisErrorInfo; +export 'package:analyzer/src/generated/source.dart' show LineInfo, Source; +export 'package:analyzer/src/lint/linter.dart' + show + dart3, + DartLinter, + Group, + LintFilter, + LintRule, + LinterContext, + LinterOptions, + Maturity, + NodeLintRegistry, + NodeLintRule, + State; +export 'package:analyzer/src/lint/pub.dart' show PSEntry, PubspecVisitor; +export 'package:analyzer/src/lint/util.dart' show FileSpelunker; +export 'package:analyzer/src/services/lint.dart' show lintRegistry; +export 'package:analyzer/src/workspace/pub.dart' show PubWorkspacePackage; + +const loggedAnalyzerErrorExitCode = 63; + +/// A version describing Dart language version 2.12.0. +// todo(pq): remove when analyzer 5.5.0 is published. +final Version dart2_12 = Version(2, 12, 0); + +Future> lintFiles( + DartLinter linter, List filesToLint) async { + // Setup an error watcher to track whether an error was logged to stderr so + // we can set the exit code accordingly. + var errorWatcher = ErrorWatchingSink(errorSink); + errorSink = errorWatcher; + + var errors = await linter.lintFiles(filesToLint); + if (errorWatcher.encounteredError) { + exitCode = loggedAnalyzerErrorExitCode; + } else if (errors.isNotEmpty) { + exitCode = _maxSeverity(errors.toList(), linter.options.filter); + } + + return errors; +} + +Iterable _filtered( + List errors, LintFilter? filter) => + (filter == null) + ? errors + : errors.where((AnalysisError e) => !filter.filter(e)); + +int _maxSeverity(List errors, LintFilter? filter) { + var max = 0; + for (var info in errors) { + _filtered(info.errors, filter).forEach((AnalysisError e) { + max = math.max(max, e.errorCode.errorSeverity.ordinal); + }); + } + return max; +} + +/// Facade for managing access to `analyzer` package APIs. +class Analyzer { + /// Shared instance. + static Analyzer facade = Analyzer(); + + /// Returns currently registered lint rules. + Iterable get registeredRules => Registry.ruleRegistry; + + /// Cache linter version; used in summary signatures. + void cacheLinterVersion() { + // todo(pq): remove (https://github.com/dart-lang/linter/issues/4418) + lint_service.linterVersion = '1.35.0'; + } + + /// Create a library name prefix based on [libraryPath], [projectRoot] and + /// current [packageName]. + String createLibraryNamePrefix( + {required String libraryPath, + String? projectRoot, + String? packageName}) => + util.createLibraryNamePrefix( + libraryPath: libraryPath, + projectRoot: projectRoot, + packageName: packageName); + + /// Register this [lint] with the analyzer's rule registry. + void register(LintRule lint) { + Registry.ruleRegistry.register(lint); + } +} + +class ErrorWatchingSink implements IOSink { + bool encounteredError = false; + + IOSink delegate; + + ErrorWatchingSink(this.delegate); + + @override + Future get done => delegate.done; + + @override + Encoding get encoding => delegate.encoding; + + @override + set encoding(Encoding encoding) { + delegate.encoding = encoding; + } + + @override + void add(List data) { + delegate.add(data); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + encounteredError = true; + delegate.addError(error, stackTrace); + } + + @override + Future addStream(Stream> stream) => delegate.addStream(stream); + + @override + Future close() => delegate.close(); + + @override + Future flush() => delegate.flush(); + + @override + void write(Object? obj) { + delegate.write(obj); + } + + @override + void writeAll(Iterable objects, [String separator = '']) { + delegate.writeAll(objects, separator); + } + + @override + void writeCharCode(int charCode) { + delegate.writeCharCode(charCode); + } + + @override + void writeln([Object? obj = '']) { + // 'Exception while using a Visitor to visit ...' ( + if (obj.toString().startsWith('Exception')) { + encounteredError = true; + } + delegate.writeln(obj); + } +} diff --git a/pkg/linter/lib/src/ast.dart b/pkg/linter/lib/src/ast.dart new file mode 100644 index 00000000000..aa14bf0f308 --- /dev/null +++ b/pkg/linter/lib/src/ast.dart @@ -0,0 +1,470 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Common AST helpers. +library; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/visitor.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/src/workspace/workspace.dart'; // ignore: implementation_imports +import 'package:path/path.dart' as path; + +import 'analyzer.dart'; +import 'utils.dart'; + +final List reservedWords = _collectReservedWords(); + +/// Returns direct children of [parent]. +List getChildren(Element parent, [String? name]) { + var children = []; + visitChildren(parent, (Element element) { + if (name == null || element.displayName == name) { + children.add(element); + } + return false; + }); + return children; +} + +/// Return the compilation unit of a node +CompilationUnit? getCompilationUnit(AstNode node) => + node.thisOrAncestorOfType(); + +/// Returns a field identifier with the given [name] in the given [decl]'s +/// variable declaration list or `null` if none is found. +Token? getFieldName(FieldDeclaration decl, String name) { + for (var v in decl.fields.variables) { + if (v.name.lexeme == name) { + return v.name; + } + } + return null; +} + +/// Returns the value of an [IntegerLiteral] or [PrefixExpression] with a +/// minus and then an [IntegerLiteral]. If a [context] is provided, +/// [SimpleIdentifier]s are evaluated as constants. For anything else, +/// returns `null`. +int? getIntValue(Expression expression, LinterContext? context) { + if (expression is PrefixExpression) { + var operand = expression.operand; + if (expression.operator.type != TokenType.MINUS) return null; + return _getIntValue(operand, context, negated: true); + } + return _getIntValue(expression, context); +} + +/// Returns the most specific AST node appropriate for associating errors. +SyntacticEntity getNodeToAnnotate(Declaration node) { + // TODO(srawlins): Convert to a switch expression over `Declaration` subtypes, + // assuming `Declaration` becomes an exhaustive type. + if (node is ClassDeclaration) { + return node.name; + } + if (node is ClassTypeAlias) { + return node.name; + } + if (node is ConstructorDeclaration) { + return node.name ?? node; + } + if (node is EnumConstantDeclaration) { + return node.name; + } + if (node is EnumDeclaration) { + return node.name; + } + if (node is ExtensionDeclaration) { + return node.name ?? node; + } + if (node is FieldDeclaration) { + return node.fields; + } + if (node is FunctionDeclaration) { + return node.name; + } + if (node is FunctionTypeAlias) { + return node.name; + } + if (node is GenericTypeAlias) { + return node.name; + } + if (node is MethodDeclaration) { + return node.name; + } + if (node is MixinDeclaration) { + return node.name; + } + if (node is TopLevelVariableDeclaration) { + return node.variables; + } + if (node is TypeParameter) { + return node.name; + } + if (node is VariableDeclaration) { + return node.name; + } + if (node is ExtensionTypeDeclaration) { + return node.name; + } + assert(false, "Unaccounted for Declaration subtype: '${node.runtimeType}'"); + return node; +} + +/// If the [node] is the finishing identifier of an assignment, return its +/// "writeElement", otherwise return its "staticElement", which might be +/// thought as the "readElement". +Element? getWriteOrReadElement(SimpleIdentifier node) { + var writeElement = _getWriteElement(node); + if (writeElement != null) { + return writeElement; + } + return node.staticElement; +} + +bool hasConstantError(LinterContext context, Expression node) { + var result = context.evaluateConstant(node); + return result.errors.isNotEmpty; +} + +/// Returns `true` if this [element] has a `@literal` annotation. +@Deprecated('prefer: element.hasLiteral') +bool hasLiteralAnnotation(Element element) => element.hasLiteral; + +/// Returns `true` if this [element] has an `@override` annotation. +@Deprecated('prefer: element.hasOverride') +bool hasOverrideAnnotation(Element element) => element.hasOverride; + +/// Returns `true` if this [node] is the child of a private compilation unit +/// member. +bool inPrivateMember(AstNode node) { + var parent = node.parent; + if (parent is NamedCompilationUnitMember) { + return isPrivate(parent.name); + } + if (parent is ExtensionDeclaration) { + return parent.name == null || isPrivate(parent.name); + } + return false; +} + +/// Returns `true` if this element is the `==` method declaration. +bool isEquals(ClassMember element) => + element is MethodDeclaration && element.name.lexeme == '=='; + +/// Returns `true` if the keyword associated with this token is `final` or +/// `const`. +bool isFinalOrConst(Token token) => + isKeyword(token, Keyword.FINAL) || isKeyword(token, Keyword.CONST); + +/// Returns `true` if this element is a `hashCode` method or field declaration. +bool isHashCode(ClassMember element) => _hasFieldOrMethod(element, 'hashCode'); + +/// Returns `true` if this element is an `index` method or field declaration. +bool isIndex(ClassMember element) => _hasFieldOrMethod(element, 'index'); + +/// Return true if this compilation unit [node] is declared within the given +/// [package]'s `lib/` directory tree. +bool isInLibDir(CompilationUnit node, WorkspacePackage? package) { + if (package == null) return false; + var cuPath = node.declaredElement?.library.source.fullName; + if (cuPath == null) return false; + var libDir = path.join(package.root, 'lib'); + return path.isWithin(libDir, cuPath); +} + +/// Return `true` if this compilation unit [node] is declared within a public +/// directory in the given [package]'s directory tree. Public dirs are the +/// `lib` and `bin` dirs. +// +// TODO: move into WorkspacePackage +bool isInPublicDir(CompilationUnit node, WorkspacePackage? package) { + if (package == null) return false; + var cuPath = node.declaredElement?.library.source.fullName; + if (cuPath == null) return false; + var libDir = path.join(package.root, 'lib'); + var binDir = path.join(package.root, 'bin'); + return path.isWithin(libDir, cuPath) || path.isWithin(binDir, cuPath); +} + +/// Returns `true` if the keyword associated with the given [token] matches +/// [keyword]. +bool isKeyword(Token token, Keyword keyword) => + token is KeywordToken && token.keyword == keyword; + +/// Returns `true` if the given [id] is a Dart keyword. +bool isKeyWord(String id) => Keyword.keywords.containsKey(id); + +/// Returns `true` if the given [ClassMember] is a method. +bool isMethod(ClassMember m) => m is MethodDeclaration; + +/// Check if the given identifier has a private name. +bool isPrivate(Token? name) => + name != null ? Identifier.isPrivateName(name.lexeme) : false; + +/// Returns `true` if the given [ClassMember] is a public method. +bool isPublicMethod(ClassMember m) { + var declaredElement = m.declaredElement; + return declaredElement != null && isMethod(m) && declaredElement.isPublic; +} + +/// Check if the given word is a Dart reserved word. +bool isReservedWord(String word) => reservedWords.contains(word); + +/// Returns `true` if the given method [declaration] is a "simple getter". +/// +/// A simple getter takes one of these basic forms: +/// +/// ```dart +/// get x => _simpleIdentifier; +/// ``` +/// +/// or +/// +/// ```dart +/// get x { +/// return _simpleIdentifier; +/// } +/// ``` +bool isSimpleGetter(MethodDeclaration declaration) { + if (!declaration.isGetter) { + return false; + } + var body = declaration.body; + if (body is ExpressionFunctionBody) { + return _checkForSimpleGetter(declaration, body.expression); + } else if (body is BlockFunctionBody) { + var block = body.block; + if (block.statements.length == 1) { + var statement = block.statements.first; + if (statement is ReturnStatement) { + return _checkForSimpleGetter(declaration, statement.expression); + } + } + } + return false; +} + +/// Returns `true` if the given [setter] is a "simple setter". +/// +/// A simple setter takes this basic form: +/// +/// ```dart +/// int _x; +/// set(int x) { +/// _x = x; +/// } +/// ``` +/// +/// or: +/// +/// ```dart +/// int _x; +/// set(int x) => _x = x; +/// ``` +/// +/// where the static type of the left and right hand sides of the assignment +/// expression are the same. +bool isSimpleSetter(MethodDeclaration setter) { + var body = setter.body; + if (body is ExpressionFunctionBody) { + return _checkForSimpleSetter(setter, body.expression); + } else if (body is BlockFunctionBody) { + var block = body.block; + if (block.statements.length == 1) { + var statement = block.statements.first; + if (statement is ExpressionStatement) { + return _checkForSimpleSetter(setter, statement.expression); + } + } + } + + return false; +} + +/// Returns `true` if the given [id] is a valid Dart identifier. +bool isValidDartIdentifier(String id) => !isKeyWord(id) && isIdentifier(id); + +/// Returns `true` if this element is a `values` method or field declaration. +bool isValues(ClassMember element) => _hasFieldOrMethod(element, 'values'); + +/// Returns `true` if the keyword associated with this token is `var`. +bool isVar(Token token) => isKeyword(token, Keyword.VAR); + +/// Return the nearest enclosing pubspec file. +File? locatePubspecFile(CompilationUnit compilationUnit) { + var fullName = compilationUnit.declaredElement?.source.fullName; + if (fullName == null) { + return null; + } + + var resourceProvider = + compilationUnit.declaredElement?.session.resourceProvider; + if (resourceProvider == null) { + return null; + } + + var file = resourceProvider.getFile(fullName); + + // Look for a pubspec.yaml file. + for (var folder in file.parent.withAncestors) { + var pubspecFile = folder.getChildAssumingFile('pubspec.yaml'); + if (pubspecFile.exists) { + return pubspecFile; + } + } + + return null; +} + +/// Uses [processor] to visit all of the children of [element]. +/// If [processor] returns `true`, then children of a child are visited too. +void visitChildren(Element element, ElementProcessor processor) { + element.visitChildren(_ElementVisitorAdapter(processor)); +} + +bool _checkForSimpleGetter(MethodDeclaration getter, Expression? expression) { + if (expression is SimpleIdentifier) { + var staticElement = expression.staticElement; + if (staticElement is PropertyAccessorElement) { + var enclosingElement = getter.declaredElement?.enclosingElement; + // Skipping library level getters, test that the enclosing element is + // the same + if (staticElement.enclosingElement == enclosingElement) { + return staticElement.isSynthetic && staticElement.variable.isPrivate; + } + } + } + return false; +} + +bool _checkForSimpleSetter(MethodDeclaration setter, Expression expression) { + if (expression is! AssignmentExpression) { + return false; + } + if (expression.operator.type != TokenType.EQ) { + return false; + } + + var leftHandSide = expression.leftHandSide; + var rightHandSide = expression.rightHandSide; + if (leftHandSide is SimpleIdentifier && rightHandSide is SimpleIdentifier) { + var leftElement = expression.writeElement; + if (leftElement is! PropertyAccessorElement || !leftElement.isSynthetic) { + return false; + } + + // To guard against setters used as type constraints + if (expression.writeType != rightHandSide.staticType) { + return false; + } + + var rightElement = rightHandSide.staticElement; + if (rightElement is! ParameterElement) { + return false; + } + + var parameters = setter.parameters?.parameters; + if (parameters != null && parameters.length == 1) { + return rightElement == parameters.first.declaredElement; + } + } + + return false; +} + +List _collectReservedWords() { + var reserved = []; + for (var entry in Keyword.keywords.entries) { + if (entry.value.isReservedWord) { + reserved.add(entry.key); + } + } + return reserved; +} + +int? _getIntValue(Expression expression, LinterContext? context, + {bool negated = false}) { + int? value; + if (expression is IntegerLiteral) { + value = expression.value; + } else if (expression is SimpleIdentifier && context != null) { + value = context.evaluateConstant(expression).value?.toIntValue(); + } + if (value is! int) return null; + + return negated ? -value : value; +} + +/// If the [node] is the target of a [CompoundAssignmentExpression], +/// return the corresponding "writeElement", which is the local variable, +/// the setter referenced with a [SimpleIdentifier] or a [PropertyAccess], +/// or the `[]=` operator. +Element? _getWriteElement(AstNode node) { + var parent = node.parent; + if (parent is AssignmentExpression && parent.leftHandSide == node) { + return parent.writeElement; + } + if (parent is PostfixExpression) { + return parent.writeElement; + } + if (parent is PrefixExpression) { + return parent.writeElement; + } + + if (parent is PrefixedIdentifier && parent.identifier == node) { + return _getWriteElement(parent); + } + + if (parent is PropertyAccess && parent.propertyName == node) { + return _getWriteElement(parent); + } + + return null; +} + +bool _hasFieldOrMethod(ClassMember element, String name) => + (element is MethodDeclaration && element.name.lexeme == name) || + (element is FieldDeclaration && getFieldName(element, name) != null); + +/// An [Element] processor function type. +/// If `true` is returned, children of [element] will be visited. +typedef ElementProcessor = bool Function(Element element); + +/// A [GeneralizingElementVisitor] adapter for [ElementProcessor]. +class _ElementVisitorAdapter extends GeneralizingElementVisitor { + final ElementProcessor processor; + + _ElementVisitorAdapter(this.processor); + + @override + void visitElement(Element element) { + var visitChildren = processor(element); + if (visitChildren) { + element.visitChildren(this); + } + } +} + +extension AstNodeExtension on AstNode { + bool get isToStringInvocationWithArguments { + var self = this; + return self is MethodInvocation && + self.methodName.name == 'toString' && + self.argumentList.arguments.isNotEmpty; + } +} + +extension ElementExtension on Element? { + // TODO(srawlins): Move to extensions.dart. + bool get isDartCorePrint { + var self = this; + return self is FunctionElement && + self.name == 'print' && + self.library.isDartCore; + } +} diff --git a/pkg/linter/lib/src/cli.dart b/pkg/linter/lib/src/cli.dart new file mode 100644 index 00000000000..544efa23b50 --- /dev/null +++ b/pkg/linter/lib/src/cli.dart @@ -0,0 +1,178 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/lint/config.dart'; // ignore: implementation_imports +import 'package:analyzer/src/lint/io.dart'; // ignore: implementation_imports +import 'package:analyzer/src/lint/registry.dart'; // ignore: implementation_imports +import 'package:args/args.dart'; + +import 'analyzer.dart'; +import 'extensions.dart'; +import 'formatter.dart'; +import 'rules.dart'; + +const processFileFailedExitCode = 65; +const unableToProcessExitCode = 64; + +String? getRoot(List paths) => + paths.length == 1 && Directory(paths.first).existsSync() + ? paths.first + : null; + +bool isLinterErrorCode(int code) => + code == unableToProcessExitCode || code == processFileFailedExitCode; + +void printUsage(ArgParser parser, IOSink out, [String? error]) { + var message = 'Lints Dart source files and pubspecs.'; + if (error != null) { + message = error; + } + + out.writeln('''$message +Usage: linter +${parser.usage} + +For more information, see https://github.com/dart-lang/linter +'''); +} + +/// Start linting from the command-line. +Future run(List args) async { + await runLinter(args, LinterOptions()); +} + +/// todo (pq): consider using `dart analyze` where possible +/// see: https://github.com/dart-lang/linter/pull/2537 +Future runLinter(List args, LinterOptions initialLintOptions) async { + // Force the rule registry to be populated. + registerLintRules(); + + var parser = ArgParser(); + parser + ..addFlag('help', + abbr: 'h', negatable: false, help: 'Show usage information.') + ..addFlag('stats', + abbr: 's', negatable: false, help: 'Show lint statistics.') + ..addFlag('benchmark', negatable: false, help: 'Show lint benchmarks.') + ..addFlag('visit-transitive-closure', + help: 'Visit the transitive closure of imported/exported libraries.') + ..addFlag('quiet', abbr: 'q', help: "Don't show individual lint errors.") + ..addFlag('machine', + help: 'Print results in a format suitable for parsing.', + negatable: false) + ..addOption('config', abbr: 'c', help: 'Use configuration from this file.') + ..addOption('dart-sdk', help: 'Custom path to a Dart SDK.') + ..addMultiOption('rules', + help: 'A list of lint rules to run. For example: ' + 'annotate_overrides, avoid_catching_errors') + // TODO(srawlins): Remove this flag; it does not work any more. + ..addOption('packages', + help: 'Path to the package resolution configuration file, which\n' + 'supplies a mapping of package names to paths.'); + + ArgResults options; + try { + options = parser.parse(args); + } on FormatException catch (err) { + printUsage(parser, errorSink, err.message); + exitCode = unableToProcessExitCode; + return; + } + + if (options['help'] as bool) { + printUsage(parser, outSink); + return; + } + + if (options.rest.isEmpty) { + printUsage(parser, errorSink, + 'Please provide at least one file or directory to lint.'); + exitCode = unableToProcessExitCode; + return; + } + + var lintOptions = initialLintOptions; + + var configFile = options['config']; + if (configFile is String) { + var config = LintConfig.parse(readFile(configFile)); + lintOptions.configure(config); + } + + var lints = options['rules']; + if (lints is Iterable && lints.isNotEmpty) { + var rules = []; + for (var lint in lints) { + var rule = Registry.ruleRegistry[lint]; + if (rule == null) { + errorSink.write('Unrecognized lint rule: $lint'); + exit(unableToProcessExitCode); + } + rules.add(rule); + } + + lintOptions.enabledLints = rules; + } + + var customSdk = options['dart-sdk']; + if (customSdk is String) { + lintOptions.dartSdkPath = customSdk; + } + + var packageConfigFile = options['packages'] as String?; + packageConfigFile = packageConfigFile?.toAbsoluteNormalizedPath(); + + var stats = options['stats'] as bool; + var benchmark = options['benchmark'] as bool; + if (stats || benchmark) { + lintOptions.enableTiming = true; + } + + lintOptions + ..packageConfigPath = packageConfigFile + ..resourceProvider = PhysicalResourceProvider.INSTANCE; + + var filesToLint = []; + for (var path in options.rest) { + filesToLint.addAll( + collectFiles(path) + .map((file) => file.path.toAbsoluteNormalizedPath()) + .map(File.new), + ); + } + + if (benchmark) { + await writeBenchmarks(outSink, filesToLint, lintOptions); + return; + } + + var linter = DartLinter(lintOptions); + + try { + var timer = Stopwatch()..start(); + var errors = await lintFiles(linter, filesToLint); + timer.stop(); + + var commonRoot = getRoot(options.rest); + var machine = options['machine'] ?? false; + var quiet = options['quiet'] ?? false; + ReportFormatter(errors, lintOptions.filter, outSink, + elapsedMs: timer.elapsedMilliseconds, + fileCount: linter.numSourcesAnalyzed, + fileRoot: commonRoot, + showStatistics: stats, + machineOutput: machine as bool, + quiet: quiet as bool) + .write(); + // ignore: avoid_catches_without_on_clauses + } catch (err, stack) { + errorSink.writeln('''An error occurred while linting + Please report it at: github.com/dart-lang/linter/issues +$err +$stack'''); + } +} diff --git a/pkg/linter/lib/src/extensions.dart b/pkg/linter/lib/src/extensions.dart new file mode 100644 index 00000000000..fb9bca2091c --- /dev/null +++ b/pkg/linter/lib/src/extensions.dart @@ -0,0 +1,505 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/dart/element/member.dart'; // ignore: implementation_imports +import 'package:collection/collection.dart'; + +import 'analyzer.dart'; +import 'util/dart_type_utilities.dart'; + +class EnumLikeClassDescription { + final Map> _enumConstants; + EnumLikeClassDescription(this._enumConstants); + + /// Returns a fresh map of the class's enum-like constant values. + Map> get enumConstants => {..._enumConstants}; +} + +extension AstNodeExtension on AstNode { + Iterable get childNodes => childEntities.whereType(); + + bool get isEffectivelyPrivate { + var node = this; + if (node.isInternal) return true; + if (node is ClassDeclaration) { + var classElement = node.declaredElement; + if (classElement != null) { + if (classElement.isSealed) return true; + if (classElement.isAbstract) { + if (classElement.isFinal) return true; + if (classElement.isInterface) return true; + } + } + } + return false; + } + + bool get isInternal { + var parent = thisOrAncestorOfType(); + if (parent == null) return false; + + var element = parent.declaredElement; + return element != null && element.hasInternal; + } + + /// Builds the list resulting from traversing the node in DFS and does not + /// include the node itself. + /// + /// It excludes the nodes for which the [excludeCriteria] returns true. If + /// [excludeCriteria] is not provided, all nodes are included. + @Deprecated( + 'This approach is slow and slated for removal. Traversal via a standard visitor is preferred.') + Iterable traverseNodesInDFS({AstNodePredicate? excludeCriteria}) { + var nodes = {}; + var nodesToVisit = List.of(childNodes); + if (excludeCriteria == null) { + while (nodesToVisit.isNotEmpty) { + var node = nodesToVisit.removeAt(0); + nodes.add(node); + nodesToVisit.insertAll(0, node.childNodes); + } + } else { + while (nodesToVisit.isNotEmpty) { + var node = nodesToVisit.removeAt(0); + if (excludeCriteria(node)) continue; + nodes.add(node); + nodesToVisit.insertAll(0, node.childNodes); + } + } + + return nodes; + } +} + +extension AstNodeNullableExtension on AstNode? { + bool get isFieldNameShortcut { + var node = this; + if (node is NullCheckPattern) node = node.parent; + if (node is NullAssertPattern) node = node.parent; + return node is PatternField && node.name != null && node.name?.name == null; + } + + /// Return `true` if the expression is null aware, or if one of its recursive + /// targets is null aware. + bool containsNullAwareInvocationInChain() { + var node = this; + if (node is PropertyAccess) { + if (node.isNullAware) return true; + return node.target.containsNullAwareInvocationInChain(); + } else if (node is MethodInvocation) { + if (node.isNullAware) return true; + return node.target.containsNullAwareInvocationInChain(); + } else if (node is IndexExpression) { + if (node.isNullAware) return true; + return node.target.containsNullAwareInvocationInChain(); + } + return false; + } +} + +extension BlockExtension on Block { + /// Returns the last statement of this block, or `null` if this is empty. + /// + /// If the last immediate statement of this block is a [Block], recurses into + /// it to find the last statement. + Statement? get lastStatement { + if (statements.isEmpty) { + return null; + } + var lastStatement = statements.last; + if (lastStatement is Block) { + return lastStatement.lastStatement; + } + return lastStatement; + } +} + +extension ClassElementExtension on ClassElement { + /// Returns an [EnumLikeClassDescription] for this if the latter is a valid + /// "enum-like" class. + /// + /// An enum-like class must meet the following requirements: + /// + /// * is concrete, + /// * has no public constructors, + /// * has no factory constructors, + /// * has two or more static const fields with the same type as the class, + /// * has no subclasses declared in the defining library. + /// + /// The returned [EnumLikeClassDescription]'s `enumConstantNames` contains all + /// of the static const fields with the same type as the class, with one + /// exception; any static const field which is marked `@Deprecated` and is + /// equal to another static const field with the same type as the class is not + /// included. Such a field is assumed to be deprecated in favor of the field + /// with equal value. + EnumLikeClassDescription? get asEnumLikeClass { + // See discussion: https://github.com/dart-lang/linter/issues/2083. + + // Must be concrete. + if (isAbstract) { + return null; + } + + // With only private non-factory constructors. + for (var constructor in constructors) { + if (!constructor.isPrivate || constructor.isFactory) { + return null; + } + } + + var type = thisType; + + // And 2 or more static const fields whose type is the enclosing class. + var enumConstantCount = 0; + var enumConstants = >{}; + for (var field in fields) { + // Ensure static const. + if (field.isSynthetic || !field.isConst || !field.isStatic) { + continue; + } + // Check for type equality. + if (field.type != type) { + continue; + } + var fieldValue = field.computeConstantValue(); + if (fieldValue == null) { + continue; + } + enumConstantCount++; + enumConstants.putIfAbsent(fieldValue, () => {}).add(field); + } + if (enumConstantCount < 2) { + return null; + } + + // And no subclasses in the defining library. + if (hasSubclassInDefiningCompilationUnit) return null; + + return EnumLikeClassDescription(enumConstants); + } + + bool get hasSubclassInDefiningCompilationUnit { + var compilationUnit = library.definingCompilationUnit; + for (var cls in compilationUnit.classes) { + InterfaceType? classType = cls.thisType; + do { + classType = classType?.superclass; + if (classType == thisType) { + return true; + } + } while (classType != null && !classType.isDartCoreObject); + } + return false; + } + + bool get isEnumLikeClass => asEnumLikeClass != null; + + /// Returns whether this class is exactly [otherName] declared in + /// [otherLibrary]. + bool isClass(String otherName, String otherLibrary) => + name == otherName && library.name == otherLibrary; +} + +extension ClassMemberListExtension on List { + MethodDeclaration? getMethod(String name) => whereType() + .firstWhereOrNull((node) => node.name.lexeme == name); +} + +extension ConstructorElementExtension on ConstructorElement { + /// Returns whether `this` is the same element as the [className] constructor + /// named [constructorName] declared in [uri]. + bool isSameAs({ + required String uri, + required String className, + required String constructorName, + }) => + library.name == uri && + enclosingElement.name == className && + name == constructorName; +} + +extension DartTypeExtension on DartType? { + bool extendsClass(String? className, String library) { + var self = this; + if (self is InterfaceType) { + return _extendsClass(self, {}, className, library); + } + return false; + } + + bool implementsAnyInterface(Iterable definitions) { + bool isAnyInterface(InterfaceType i) => + definitions.any((d) => i.isSameAs(d.name, d.library)); + + var typeToCheck = this; + if (typeToCheck is TypeParameterType) { + typeToCheck = typeToCheck.typeForInterfaceCheck; + } + if (typeToCheck is InterfaceType) { + return isAnyInterface(typeToCheck) || + !typeToCheck.element.isSynthetic && + typeToCheck.element.allSupertypes.any(isAnyInterface); + } else { + return false; + } + } + + bool implementsInterface(String interface, String library) { + var self = this; + if (self is! InterfaceType) { + return false; + } + bool predicate(InterfaceType i) => i.isSameAs(interface, library); + var element = self.element; + return predicate(self) || + !element.isSynthetic && element.allSupertypes.any(predicate); + } + + /// Returns whether `this` is the same element as [interface], declared in + /// [library]. + bool isSameAs(String? interface, String? library) { + var self = this; + return self is InterfaceType && + self.element.name == interface && + self.element.library.name == library; + } + + static bool _extendsClass( + InterfaceType? type, + Set seenElements, + String? className, + String? library) => + type != null && + seenElements.add(type.element) && + (type.isSameAs(className, library) || + _extendsClass(type.superclass, seenElements, className, library)); +} + +extension ElementExtension on Element { + Element get canonicalElement { + var self = this; + if (self is PropertyAccessorElement) { + var variable = self.variable; + if (variable is FieldMember) { + // A field element defined in a parameterized type where the values of + // the type parameters are known. + // + // This concept should be invisible when comparing FieldElements, but a + // bug in the analyzer causes FieldElements to not evaluate as + // equivalent to equivalent FieldMembers. See + // https://github.com/dart-lang/sdk/issues/35343. + return variable.declaration; + } else { + return variable; + } + } else { + return self; + } + } +} + +extension ExpressionExtension on Expression? { + bool get isNullLiteral => this?.unParenthesized is NullLiteral; +} + +extension FieldDeclarationExtension on FieldDeclaration { + bool get isInvalidExtensionTypeField => + !isStatic && parent is ExtensionTypeDeclaration; +} + +extension InhertanceManager3Extension on InheritanceManager3 { + /// Returns the class member that is overridden by [member], if there is one, + /// as defined by [getInherited]. + ExecutableElement? overriddenMember(Element? member) { + if (member == null) { + return null; + } + + var interfaceElement = member.thisOrAncestorOfType(); + if (interfaceElement == null) { + return null; + } + var name = member.name; + if (name == null) { + return null; + } + + var libraryUri = interfaceElement.library.source.uri; + return getInherited(interfaceElement.thisType, Name(libraryUri, name)); + } +} + +extension InterfaceElementExtension on InterfaceElement { + /// Returns whether this element is exactly [otherName] declared in + /// [otherLibrary]. + bool isClass(String otherName, String otherLibrary) => + name == otherName && library.name == otherLibrary; +} + +extension InterfaceTypeExtension on InterfaceType { + /// Returns the collection of all interfaces that this type implements, + /// including itself. + Iterable get implementedInterfaces { + void searchSupertypes( + InterfaceType? type, + Set alreadyVisited, + List interfaceTypes) { + if (type == null || !alreadyVisited.add(type.element)) { + return; + } + interfaceTypes.add(type); + searchSupertypes(type.superclass, alreadyVisited, interfaceTypes); + for (var interface in type.interfaces) { + searchSupertypes(interface, alreadyVisited, interfaceTypes); + } + for (var mixin in type.mixins) { + searchSupertypes(mixin, alreadyVisited, interfaceTypes); + } + } + + var interfaceTypes = []; + searchSupertypes(this, {}, interfaceTypes); + return interfaceTypes; + } +} + +extension MethodDeclarationExtension on MethodDeclaration { + bool get hasInheritedMethod => lookUpInheritedMethod() != null; + + /// Returns whether this method is an override of a method in any supertype. + bool get isOverride { + var name = declaredElement?.name; + if (name == null) { + return false; + } + var parentElement = declaredElement?.enclosingElement; + if (parentElement is! InterfaceElement) { + return false; + } + var parentLibrary = parentElement.library; + + if (isGetter) { + // Search supertypes for a getter of the same name. + return parentElement.allSupertypes + .any((t) => t.lookUpGetter2(name, parentLibrary) != null); + } else if (isSetter) { + // Search supertypes for a setter of the same name. + return parentElement.allSupertypes + .any((t) => t.lookUpSetter2(name, parentLibrary) != null); + } else { + // Search supertypes for a method of the same name. + return parentElement.allSupertypes + .any((t) => t.lookUpMethod2(name, parentLibrary) != null); + } + } + + PropertyAccessorElement? lookUpGetter() { + var declaredElement = this.declaredElement; + if (declaredElement == null) { + return null; + } + var parent = declaredElement.enclosingElement; + if (parent is InterfaceElement) { + return parent.lookUpGetter(name.lexeme, declaredElement.library); + } + if (parent is ExtensionElement) { + return parent.getGetter(name.lexeme); + } + return null; + } + + PropertyAccessorElement? lookUpInheritedConcreteGetter() { + var declaredElement = this.declaredElement; + if (declaredElement == null) { + return null; + } + var parent = declaredElement.enclosingElement; + if (parent is InterfaceElement) { + return parent.lookUpInheritedConcreteGetter( + name.lexeme, declaredElement.library); + } + // Extensions don't inherit. + return null; + } + + MethodElement? lookUpInheritedConcreteMethod() { + var declaredElement = this.declaredElement; + if (declaredElement != null) { + var parent = declaredElement.enclosingElement; + if (parent is InterfaceElement) { + return parent.lookUpInheritedConcreteMethod( + name.lexeme, declaredElement.library); + } + } + // Extensions don't inherit. + return null; + } + + PropertyAccessorElement? lookUpInheritedConcreteSetter() { + var declaredElement = this.declaredElement; + if (declaredElement != null) { + var parent = declaredElement.enclosingElement; + if (parent is InterfaceElement) { + return parent.lookUpInheritedConcreteSetter( + name.lexeme, declaredElement.library); + } + } + // Extensions don't inherit. + return null; + } + + MethodElement? lookUpInheritedMethod() { + var declaredElement = this.declaredElement; + if (declaredElement != null) { + var parent = declaredElement.enclosingElement; + if (parent is InterfaceElement) { + return parent.lookUpInheritedMethod( + name.lexeme, declaredElement.library); + } + } + return null; + } +} + +extension NullableAstNodeExtension on AstNode? { + Element? get canonicalElement { + var self = this; + if (self is Expression) { + var node = self.unParenthesized; + if (node is Identifier) { + return node.staticElement?.canonicalElement; + } else if (node is PropertyAccess) { + return node.propertyName.staticElement?.canonicalElement; + } + } + return null; + } +} + +extension StringExtension on String { + String toAbsoluteNormalizedPath() { + var pathContext = PhysicalResourceProvider.INSTANCE.pathContext; + return pathContext.normalize(pathContext.absolute(this)); + } +} + +extension TokenExtension on Token? { + bool get isFinal => this?.keyword == Keyword.FINAL; +} + +extension TokenTypeExtension on TokenType { + TokenType get inverted => switch (this) { + TokenType.LT_EQ => TokenType.GT_EQ, + TokenType.LT => TokenType.GT, + TokenType.GT => TokenType.LT, + TokenType.GT_EQ => TokenType.LT_EQ, + _ => this + }; +} diff --git a/pkg/linter/lib/src/formatter.dart b/pkg/linter/lib/src/formatter.dart new file mode 100644 index 00000000000..ee04e4a8240 --- /dev/null +++ b/pkg/linter/lib/src/formatter.dart @@ -0,0 +1,341 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; +import 'dart:math'; + +import 'package:analyzer/error/error.dart'; + +import 'analyzer.dart'; +import 'util/charcodes.dart' show $backslash, $pipe; +import 'util/score_utils.dart'; + +// Number of times to perform linting to get stable benchmarks. +const benchmarkRuns = 10; + +String getLineContents(int? lineNumber, AnalysisError error) { + var path = error.source.fullName; + var file = File(path); + String failureDetails; + if (!file.existsSync()) { + failureDetails = 'file at $path does not exist'; + } else { + var lines = file.readAsLinesSync(); + var lineIndex = lineNumber! - 1; + if (lines.length > lineIndex) { + return lines[lineIndex]; + } + failureDetails = + 'line index ($lineIndex), outside of file line range (${lines.length})'; + } + throw StateError('Unable to get contents for line: $failureDetails'); +} + +String pluralize(String word, int? count) => + "$count ${count == 1 ? word : '${word}s'}"; +String shorten(String? fileRoot, String fullName) { + if (fileRoot == null || !fullName.startsWith(fileRoot)) { + return fullName; + } + return fullName.substring(fileRoot.length); +} + +Future writeBenchmarks( + IOSink out, List filesToLint, LinterOptions lintOptions) async { + var timings = {}; + for (var i = 0; i < benchmarkRuns; ++i) { + await lintFiles(DartLinter(lintOptions), filesToLint); + lintRegistry.timers.forEach((n, t) { + var timing = t.elapsedMilliseconds; + var previous = timings[n]; + if (previous == null) { + timings[n] = timing; + } else { + timings[n] = min(previous, timing); + } + }); + } + + var coreRuleset = await coreRules; + var recommendedRuleset = await recommendedRules; + var flutterRuleset = await flutterRules; + + var stats = timings.keys.map((t) { + var sets = []; + if (coreRuleset.contains(t)) { + sets.add('core'); + } + if (recommendedRuleset.contains(t)) { + sets.add('recommended'); + } + if (flutterRuleset.contains(t)) { + sets.add('flutter'); + } + + var details = sets.isEmpty ? '' : " [${sets.join(', ')}]"; + return _Stat('$t$details', timings[t] ?? 0); + }).toList(); + _writeTimings(out, stats, 0); +} + +String _escapePipe(String input) { + var result = StringBuffer(); + for (var c in input.codeUnits) { + if (c == $backslash || c == $pipe) { + result.write('\\'); + } + result.writeCharCode(c); + } + return result.toString(); +} + +void _writeTimings(IOSink out, List<_Stat> timings, int summaryLength) { + var names = timings.map((s) => s.name).toList(); + + var longestName = + names.fold(0, (prev, element) => max(prev, element.length)); + var longestTime = 8; + var tableWidth = max(summaryLength, longestName + longestTime); + var pad = tableWidth - longestName; + var line = ''.padLeft(tableWidth, '-'); + + out + ..writeln() + ..writeln(line) + ..writeln('${'Timings'.padRight(longestName)}${'ms'.padLeft(pad)}') + ..writeln(line); + var totalTime = 0; + + timings.sort(); + for (var stat in timings) { + totalTime += stat.elapsed; + out.writeln( + '${stat.name.padRight(longestName)}${stat.elapsed.toString().padLeft(pad)}'); + } + out + ..writeln(line) + ..writeln( + '${'Total'.padRight(longestName)}${totalTime.toString().padLeft(pad)}') + ..writeln(line); +} + +class DetailedReporter extends SimpleFormatter { + DetailedReporter(super.errors, super.filter, super.out, + {super.fileCount, + super.elapsedMs, + super.fileRoot, + super.showStatistics, + super.machineOutput, + super.quiet}); + + @override + void writeLint(AnalysisError error, {int? offset, int? line, int? column}) { + super.writeLint(error, offset: offset, column: column, line: line); + + if (!machineOutput) { + var contents = getLineContents(line, error); + out.writeln(contents); + + var spaces = column! - 1; + var arrows = max(1, min(error.length, contents.length - spaces)); + + var result = '${" " * spaces}${"^" * arrows}'; + out.writeln(result); + } + } +} + +abstract class ReportFormatter { + factory ReportFormatter( + Iterable errors, LintFilter? filter, IOSink out, + {int? fileCount, + int? elapsedMs, + String? fileRoot, + bool showStatistics = false, + bool machineOutput = false, + bool quiet = false}) => + DetailedReporter(errors, filter, out, + fileCount: fileCount, + fileRoot: fileRoot, + elapsedMs: elapsedMs, + showStatistics: showStatistics, + machineOutput: machineOutput, + quiet: quiet); + + void write(); +} + +/// Simple formatter suitable for subclassing. +class SimpleFormatter implements ReportFormatter { + final IOSink out; + final Iterable errors; + final LintFilter? filter; + + int errorCount = 0; + int filteredLintCount = 0; + + final int? fileCount; + final int? elapsedMs; + final String? fileRoot; + final bool showStatistics; + final bool machineOutput; + final bool quiet; + + /// Cached for the purposes of statistics report formatting. + int _summaryLength = 0; + + Map stats = {}; + + SimpleFormatter(this.errors, this.filter, this.out, + {this.fileCount, + this.fileRoot, + this.elapsedMs, + this.showStatistics = false, + this.quiet = false, + this.machineOutput = false}); + + /// Override to influence error sorting + int compare(AnalysisError error1, AnalysisError error2) { + // Severity + var compare = error2.errorCode.errorSeverity + .compareTo(error1.errorCode.errorSeverity); + if (compare != 0) { + return compare; + } + // Path + compare = Comparable.compare(error1.source.fullName.toLowerCase(), + error2.source.fullName.toLowerCase()); + if (compare != 0) { + return compare; + } + // Offset + return error1.offset - error2.offset; + } + + @override + void write() { + writeLints(); + writeSummary(); + if (showStatistics) { + out.writeln(); + writeStatistics(); + } + out.writeln(); + } + + void writeCounts() { + var codes = stats.keys.toList()..sort(); + var largestCountGuess = 8; + var longest = + codes.fold(0, (int prev, element) => max(prev, element.length)); + var tableWidth = max(_summaryLength, longest + largestCountGuess); + var pad = tableWidth - longest; + var line = ''.padLeft(tableWidth, '-'); + out + ..writeln(line) + ..writeln('Counts') + ..writeln(line); + for (var code in codes) { + out + ..write(code.padRight(longest)) + ..writeln(stats[code].toString().padLeft(pad)); + } + out.writeln(line); + } + + void writeLint(AnalysisError error, {int? offset, int? line, int? column}) { + if (machineOutput) { + //INFO|LINT|constant_identifier_names|test/engine_test.dart|91|22|3|Prefer using lowerCamelCase for constant names. + out + ..write(error.errorCode.errorSeverity) + ..write('|') + ..write(error.errorCode.type) + ..write('|') + ..write(error.errorCode.name) + ..write('|') + ..write(_escapePipe(error.source.fullName)) + ..write('|') + ..write(line) + ..write('|') + ..write(column) + ..write('|') + ..write(error.length) + ..write('|') + ..writeln(_escapePipe(error.message)); + } else { + // test/engine_test.dart 452:9 [lint] DO name types using UpperCamelCase. + out + ..write('${shorten(fileRoot, error.source.fullName)} ') + ..write('$line:$column ') + ..writeln('[${error.errorCode.type.displayName}] ${error.message}'); + } + } + + void writeLints() { + var filter = this.filter; + for (var info in errors) { + for (var e in (info.errors.toList()..sort(compare))) { + if (filter != null && filter.filter(e)) { + filteredLintCount++; + } else { + ++errorCount; + if (!quiet) { + _writeLint(e, info.lineInfo); + } + _recordStats(e); + } + } + } + if (!quiet) { + out.writeln(); + } + } + + void writeStatistics() { + writeCounts(); + writeTimings(); + } + + void writeSummary() { + var summary = '${pluralize("file", fileCount)} analyzed, ' + '${pluralize("issue", errorCount)} found' + "${filteredLintCount == 0 ? '' : ' ($filteredLintCount filtered)'}, in $elapsedMs ms."; + out.writeln(summary); + // Cache for output table sizing + _summaryLength = summary.length; + } + + void writeTimings() { + var timers = lintRegistry.timers; + var timings = timers.keys + .map((t) => _Stat(t, timers[t]?.elapsedMilliseconds ?? 0)) + .toList(); + _writeTimings(out, timings, _summaryLength); + } + + void _recordStats(AnalysisError error) { + var codeName = error.errorCode.name; + stats.putIfAbsent(codeName, () => 0); + stats[codeName] = stats[codeName]! + 1; + } + + void _writeLint(AnalysisError error, LineInfo lineInfo) { + var offset = error.offset; + var location = lineInfo.getLocation(offset); + var line = location.lineNumber; + var column = location.columnNumber; + + writeLint(error, offset: offset, column: column, line: line); + } +} + +class _Stat implements Comparable<_Stat> { + final String name; + final int elapsed; + + _Stat(this.name, this.elapsed); + + @override + int compareTo(_Stat other) => other.elapsed - elapsed; +} diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart new file mode 100644 index 00000000000..5adcef427cc --- /dev/null +++ b/pkg/linter/lib/src/rules.dart @@ -0,0 +1,464 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'analyzer.dart'; +import 'rules/always_declare_return_types.dart'; +import 'rules/always_put_control_body_on_new_line.dart'; +import 'rules/always_put_required_named_parameters_first.dart'; +import 'rules/always_require_non_null_named_parameters.dart'; +import 'rules/always_specify_types.dart'; +import 'rules/always_use_package_imports.dart'; +import 'rules/annotate_overrides.dart'; +import 'rules/avoid_annotating_with_dynamic.dart'; +import 'rules/avoid_as.dart'; +import 'rules/avoid_bool_literals_in_conditional_expressions.dart'; +import 'rules/avoid_catches_without_on_clauses.dart'; +import 'rules/avoid_catching_errors.dart'; +import 'rules/avoid_classes_with_only_static_members.dart'; +import 'rules/avoid_double_and_int_checks.dart'; +import 'rules/avoid_dynamic_calls.dart'; +import 'rules/avoid_empty_else.dart'; +import 'rules/avoid_equals_and_hash_code_on_mutable_classes.dart'; +import 'rules/avoid_escaping_inner_quotes.dart'; +import 'rules/avoid_field_initializers_in_const_classes.dart'; +import 'rules/avoid_final_parameters.dart'; +import 'rules/avoid_function_literals_in_foreach_calls.dart'; +import 'rules/avoid_implementing_value_types.dart'; +import 'rules/avoid_init_to_null.dart'; +import 'rules/avoid_js_rounded_ints.dart'; +import 'rules/avoid_multiple_declarations_per_line.dart'; +import 'rules/avoid_null_checks_in_equality_operators.dart'; +import 'rules/avoid_positional_boolean_parameters.dart'; +import 'rules/avoid_print.dart'; +import 'rules/avoid_private_typedef_functions.dart'; +import 'rules/avoid_redundant_argument_values.dart'; +import 'rules/avoid_relative_lib_imports.dart'; +import 'rules/avoid_renaming_method_parameters.dart'; +import 'rules/avoid_return_types_on_setters.dart'; +import 'rules/avoid_returning_null.dart'; +import 'rules/avoid_returning_null_for_future.dart'; +import 'rules/avoid_returning_null_for_void.dart'; +import 'rules/avoid_returning_this.dart'; +import 'rules/avoid_setters_without_getters.dart'; +import 'rules/avoid_shadowing_type_parameters.dart'; +import 'rules/avoid_single_cascade_in_expression_statements.dart'; +import 'rules/avoid_slow_async_io.dart'; +import 'rules/avoid_type_to_string.dart'; +import 'rules/avoid_types_as_parameter_names.dart'; +import 'rules/avoid_types_on_closure_parameters.dart'; +import 'rules/avoid_unnecessary_containers.dart'; +import 'rules/avoid_unused_constructor_parameters.dart'; +import 'rules/avoid_void_async.dart'; +import 'rules/avoid_web_libraries_in_flutter.dart'; +import 'rules/await_only_futures.dart'; +import 'rules/camel_case_extensions.dart'; +import 'rules/camel_case_types.dart'; +import 'rules/cancel_subscriptions.dart'; +import 'rules/cascade_invocations.dart'; +import 'rules/cast_nullable_to_non_nullable.dart'; +import 'rules/close_sinks.dart'; +import 'rules/collection_methods_unrelated_type.dart'; +import 'rules/combinators_ordering.dart'; +import 'rules/comment_references.dart'; +import 'rules/conditional_uri_does_not_exist.dart'; +import 'rules/constant_identifier_names.dart'; +import 'rules/control_flow_in_finally.dart'; +import 'rules/curly_braces_in_flow_control_structures.dart'; +import 'rules/dangling_library_doc_comments.dart'; +import 'rules/deprecated_consistency.dart'; +import 'rules/deprecated_member_use_from_same_package.dart'; +import 'rules/diagnostic_describe_all_properties.dart'; +import 'rules/directives_ordering.dart'; +import 'rules/discarded_futures.dart'; +import 'rules/do_not_use_environment.dart'; +import 'rules/empty_catches.dart'; +import 'rules/empty_constructor_bodies.dart'; +import 'rules/empty_statements.dart'; +import 'rules/enable_null_safety.dart'; +import 'rules/eol_at_end_of_file.dart'; +import 'rules/exhaustive_cases.dart'; +import 'rules/file_names.dart'; +import 'rules/flutter_style_todos.dart'; +import 'rules/hash_and_equals.dart'; +import 'rules/implementation_imports.dart'; +import 'rules/implicit_call_tearoffs.dart'; +import 'rules/implicit_reopen.dart'; +import 'rules/invalid_case_patterns.dart'; +import 'rules/invariant_booleans.dart'; +import 'rules/iterable_contains_unrelated_type.dart'; +import 'rules/join_return_with_assignment.dart'; +import 'rules/leading_newlines_in_multiline_strings.dart'; +import 'rules/library_annotations.dart'; +import 'rules/library_names.dart'; +import 'rules/library_prefixes.dart'; +import 'rules/library_private_types_in_public_api.dart'; +import 'rules/lines_longer_than_80_chars.dart'; +import 'rules/list_remove_unrelated_type.dart'; +import 'rules/literal_only_boolean_expressions.dart'; +import 'rules/matching_super_parameters.dart'; +import 'rules/missing_whitespace_between_adjacent_strings.dart'; +import 'rules/no_adjacent_strings_in_list.dart'; +import 'rules/no_default_cases.dart'; +import 'rules/no_duplicate_case_values.dart'; +import 'rules/no_leading_underscores_for_library_prefixes.dart'; +import 'rules/no_leading_underscores_for_local_identifiers.dart'; +import 'rules/no_literal_bool_comparisons.dart'; +import 'rules/no_logic_in_create_state.dart'; +import 'rules/no_runtimeType_toString.dart'; +import 'rules/no_self_assignments.dart'; +import 'rules/no_wildcard_variable_uses.dart'; +import 'rules/non_constant_identifier_names.dart'; +import 'rules/noop_primitive_operations.dart'; +import 'rules/null_check_on_nullable_type_parameter.dart'; +import 'rules/null_closures.dart'; +import 'rules/omit_local_variable_types.dart'; +import 'rules/one_member_abstracts.dart'; +import 'rules/only_throw_errors.dart'; +import 'rules/overridden_fields.dart'; +import 'rules/package_api_docs.dart'; +import 'rules/package_prefixed_library_names.dart'; +import 'rules/parameter_assignments.dart'; +import 'rules/prefer_adjacent_string_concatenation.dart'; +import 'rules/prefer_asserts_in_initializer_lists.dart'; +import 'rules/prefer_asserts_with_message.dart'; +import 'rules/prefer_bool_in_asserts.dart'; +import 'rules/prefer_collection_literals.dart'; +import 'rules/prefer_conditional_assignment.dart'; +import 'rules/prefer_const_constructors.dart'; +import 'rules/prefer_const_constructors_in_immutables.dart'; +import 'rules/prefer_const_declarations.dart'; +import 'rules/prefer_const_literals_to_create_immutables.dart'; +import 'rules/prefer_constructors_over_static_methods.dart'; +import 'rules/prefer_contains.dart'; +import 'rules/prefer_double_quotes.dart'; +import 'rules/prefer_equal_for_default_values.dart'; +import 'rules/prefer_expression_function_bodies.dart'; +import 'rules/prefer_final_fields.dart'; +import 'rules/prefer_final_in_for_each.dart'; +import 'rules/prefer_final_locals.dart'; +import 'rules/prefer_final_parameters.dart'; +import 'rules/prefer_for_elements_to_map_fromIterable.dart'; +import 'rules/prefer_foreach.dart'; +import 'rules/prefer_function_declarations_over_variables.dart'; +import 'rules/prefer_generic_function_type_aliases.dart'; +import 'rules/prefer_if_elements_to_conditional_expressions.dart'; +import 'rules/prefer_if_null_operators.dart'; +import 'rules/prefer_initializing_formals.dart'; +import 'rules/prefer_inlined_adds.dart'; +import 'rules/prefer_int_literals.dart'; +import 'rules/prefer_interpolation_to_compose_strings.dart'; +import 'rules/prefer_is_empty.dart'; +import 'rules/prefer_is_not_empty.dart'; +import 'rules/prefer_is_not_operator.dart'; +import 'rules/prefer_iterable_whereType.dart'; +import 'rules/prefer_mixin.dart'; +import 'rules/prefer_null_aware_method_calls.dart'; +import 'rules/prefer_null_aware_operators.dart'; +import 'rules/prefer_relative_imports.dart'; +import 'rules/prefer_single_quotes.dart'; +import 'rules/prefer_spread_collections.dart'; +import 'rules/prefer_typing_uninitialized_variables.dart'; +import 'rules/prefer_void_to_null.dart'; +import 'rules/provide_deprecation_message.dart'; +import 'rules/pub/depend_on_referenced_packages.dart'; +import 'rules/pub/package_names.dart'; +import 'rules/pub/secure_pubspec_urls.dart'; +import 'rules/pub/sort_pub_dependencies.dart'; +import 'rules/public_member_api_docs.dart'; +import 'rules/recursive_getters.dart'; +import 'rules/require_trailing_commas.dart'; +import 'rules/sized_box_for_whitespace.dart'; +import 'rules/sized_box_shrink_expand.dart'; +import 'rules/slash_for_doc_comments.dart'; +import 'rules/sort_child_properties_last.dart'; +import 'rules/sort_constructors_first.dart'; +import 'rules/sort_unnamed_constructors_first.dart'; +import 'rules/super_goes_last.dart'; +import 'rules/test_types_in_equals.dart'; +import 'rules/throw_in_finally.dart'; +import 'rules/tighten_type_of_initializing_formals.dart'; +import 'rules/type_annotate_public_apis.dart'; +import 'rules/type_init_formals.dart'; +import 'rules/type_literal_in_constant_pattern.dart'; +import 'rules/unawaited_futures.dart'; +import 'rules/unnecessary_await_in_return.dart'; +import 'rules/unnecessary_brace_in_string_interps.dart'; +import 'rules/unnecessary_breaks.dart'; +import 'rules/unnecessary_const.dart'; +import 'rules/unnecessary_constructor_name.dart'; +import 'rules/unnecessary_final.dart'; +import 'rules/unnecessary_getters_setters.dart'; +import 'rules/unnecessary_lambdas.dart'; +import 'rules/unnecessary_late.dart'; +import 'rules/unnecessary_library_directive.dart'; +import 'rules/unnecessary_new.dart'; +import 'rules/unnecessary_null_aware_assignments.dart'; +import 'rules/unnecessary_null_aware_operator_on_extension_on_nullable.dart'; +import 'rules/unnecessary_null_checks.dart'; +import 'rules/unnecessary_null_in_if_null_operators.dart'; +import 'rules/unnecessary_nullable_for_final_variable_declarations.dart'; +import 'rules/unnecessary_overrides.dart'; +import 'rules/unnecessary_parenthesis.dart'; +import 'rules/unnecessary_raw_strings.dart'; +import 'rules/unnecessary_statements.dart'; +import 'rules/unnecessary_string_escapes.dart'; +import 'rules/unnecessary_string_interpolations.dart'; +import 'rules/unnecessary_this.dart'; +import 'rules/unnecessary_to_list_in_spreads.dart'; +import 'rules/unreachable_from_main.dart'; +import 'rules/unrelated_type_equality_checks.dart'; +import 'rules/unsafe_html.dart'; +import 'rules/use_build_context_synchronously.dart'; +import 'rules/use_colored_box.dart'; +import 'rules/use_decorated_box.dart'; +import 'rules/use_enums.dart'; +import 'rules/use_full_hex_values_for_flutter_colors.dart'; +import 'rules/use_function_type_syntax_for_parameters.dart'; +import 'rules/use_if_null_to_convert_nulls_to_bools.dart'; +import 'rules/use_is_even_rather_than_modulo.dart'; +import 'rules/use_key_in_widget_constructors.dart'; +import 'rules/use_late_for_private_fields_and_variables.dart'; +import 'rules/use_named_constants.dart'; +import 'rules/use_raw_strings.dart'; +import 'rules/use_rethrow_when_possible.dart'; +import 'rules/use_setters_to_change_properties.dart'; +import 'rules/use_string_buffers.dart'; +import 'rules/use_string_in_part_of_directives.dart'; +import 'rules/use_super_parameters.dart'; +import 'rules/use_test_throws_matchers.dart'; +import 'rules/use_to_and_as_if_applicable.dart'; +import 'rules/valid_regexps.dart'; +import 'rules/void_checks.dart'; + +void registerLintRules({bool inTestMode = false}) { + Analyzer.facade.cacheLinterVersion(); + Analyzer.facade + ..register(AlwaysDeclareReturnTypes()) + ..register(AlwaysPutControlBodyOnNewLine()) + ..register(AlwaysPutRequiredNamedParametersFirst()) + ..register(AlwaysRequireNonNullNamedParameters()) + ..register(AlwaysSpecifyTypes()) + ..register(AlwaysUsePackageImports()) + ..register(AnnotateOverrides()) + ..register(AvoidAnnotatingWithDynamic()) + ..register(AvoidAs()) + ..register(AvoidBoolLiteralsInConditionalExpressions()) + ..register(AvoidCatchesWithoutOnClauses()) + ..register(AvoidCatchingErrors()) + ..register(AvoidClassesWithOnlyStaticMembers()) + ..register(AvoidDoubleAndIntChecks()) + ..register(AvoidDynamicCalls()) + ..register(AvoidEmptyElse()) + ..register(AvoidEqualsAndHashCodeOnMutableClasses()) + ..register(AvoidEscapingInnerQuotes()) + ..register(AvoidFieldInitializersInConstClasses()) + ..register(AvoidFinalParameters()) + ..register(AvoidFunctionLiteralsInForeachCalls()) + ..register(AvoidImplementingValueTypes()) + ..register(AvoidInitToNull()) + ..register(AvoidJsRoundedInts()) + ..register(AvoidMultipleDeclarationsPerLine()) + ..register(AvoidNullChecksInEqualityOperators()) + ..register(AvoidPositionalBooleanParameters()) + ..register(AvoidPrint()) + ..register(AvoidPrivateTypedefFunctions()) + ..register(AvoidRedundantArgumentValues()) + ..register(AvoidRelativeLibImports()) + ..register(AvoidRenamingMethodParameters()) + ..register(AvoidReturnTypesOnSetters()) + ..register(AvoidReturningNull()) + ..register(AvoidReturningNullForFuture()) + ..register(AvoidReturningNullForVoid()) + ..register(AvoidReturningThis()) + ..register(AvoidSettersWithoutGetters()) + ..register(AvoidShadowingTypeParameters()) + ..register(AvoidSingleCascadeInExpressionStatements()) + ..register(AvoidSlowAsyncIo()) + ..register(AvoidTypeToString()) + ..register(AvoidTypesAsParameterNames()) + ..register(AvoidTypesOnClosureParameters()) + ..register(AvoidUnnecessaryContainers()) + ..register(AvoidUnusedConstructorParameters()) + ..register(AvoidVoidAsync()) + ..register(AvoidWebLibrariesInFlutter()) + ..register(AwaitOnlyFutures()) + ..register(CamelCaseExtensions()) + ..register(CamelCaseTypes()) + ..register(CancelSubscriptions()) + ..register(CascadeInvocations()) + ..register(CastNullableToNonNullable()) + ..register(CloseSinks()) + ..register(CollectionMethodsUnrelatedType()) + ..register(CombinatorsOrdering()) + ..register(CommentReferences()) + ..register(ConditionalUriDoesNotExist()) + ..register(ConstantIdentifierNames()) + ..register(ControlFlowInFinally()) + ..register(CurlyBracesInFlowControlStructures()) + ..register(DanglingLibraryDocComments()) + ..register(DependOnReferencedPackages()) + ..register(DeprecatedConsistency()) + ..register(DeprecatedMemberUseFromSamePackage()) + ..register(DiagnosticDescribeAllProperties()) + ..register(DirectivesOrdering()) + ..register(DiscardedFutures()) + ..register(DoNotUseEnvironment()) + ..register(EmptyCatches()) + ..register(EmptyConstructorBodies()) + ..register(EmptyStatements()) + ..register(EnableNullSafety()) + ..register(EolAtEndOfFile()) + ..register(ExhaustiveCases()) + ..register(FileNames()) + ..register(FlutterStyleTodos()) + ..register(HashAndEquals()) + ..register(ImplementationImports()) + ..register(ImplicitCallTearoffs()) + ..register(ImplicitReopen()) + ..register(InvalidCasePatterns()) + ..register(InvariantBooleans()) + ..register(IterableContainsUnrelatedType()) + ..register(JoinReturnWithAssignment()) + ..register(LeadingNewlinesInMultilineStrings()) + ..register(LibraryAnnotations()) + ..register(LibraryNames()) + ..register(LibraryPrefixes()) + ..register(LibraryPrivateTypesInPublicApi()) + ..register(LinesLongerThan80Chars()) + ..register(ListRemoveUnrelatedType()) + ..register(LiteralOnlyBooleanExpressions()) + ..register(MatchingSuperParameters()) + ..register(MissingWhitespaceBetweenAdjacentStrings()) + ..register(NoAdjacentStringsInList()) + ..register(NoDefaultCases()) + ..register(NoDuplicateCaseValues()) + ..register(NoLeadingUnderscoresForLibraryPrefixes()) + ..register(NoLeadingUnderscoresForLocalIdentifiers()) + ..register(NoLiteralBoolComparisons()) + ..register(NoLogicInCreateState()) + ..register(NoRuntimeTypeToString()) + ..register(NoSelfAssignments()) + ..register(NoWildcardVariableUses()) + ..register(NonConstantIdentifierNames()) + ..register(NoopPrimitiveOperations()) + ..register(NullCheckOnNullableTypeParameter()) + ..register(NullClosures()) + ..register(OmitLocalVariableTypes()) + ..register(OneMemberAbstracts()) + ..register(OnlyThrowErrors()) + ..register(OverriddenFields()) + ..register(PackageApiDocs()) + ..register(PackageNames()) + ..register(PackagePrefixedLibraryNames()) + ..register(ParameterAssignments()) + ..register(PreferAdjacentStringConcatenation()) + ..register(PreferAssertsInInitializerLists()) + ..register(PreferAssertsWithMessage()) + ..register(PreferBoolInAsserts()) + ..register(PreferCollectionLiterals()) + ..register(PreferConditionalAssignment()) + ..register(PreferConstConstructors()) + ..register(PreferConstConstructorsInImmutables()) + ..register(PreferConstDeclarations()) + ..register(PreferConstLiteralsToCreateImmutables()) + ..register(PreferConstructorsOverStaticMethods()) + ..register(PreferContains()) + ..register(PreferDoubleQuotes()) + ..register(PreferEqualForDefaultValues()) + ..register(PreferExpressionFunctionBodies()) + ..register(PreferFinalFields()) + ..register(PreferFinalInForEach()) + ..register(PreferFinalLocals()) + ..register(PreferFinalParameters()) + ..register(PreferForElementsToMapFromIterable()) + ..register(PreferForeach()) + ..register(PreferFunctionDeclarationsOverVariables()) + ..register(PreferGenericFunctionTypeAliases()) + ..register(PreferIfElementsToConditionalExpressions()) + ..register(PreferIfNullOperators()) + ..register(PreferInitializingFormals()) + ..register(PreferInlinedAdds()) + ..register(PreferIntLiterals()) + ..register(PreferInterpolationToComposeStrings()) + ..register(PreferIsEmpty()) + ..register(PreferIsNotEmpty()) + ..register(PreferIsNotOperator()) + ..register(PreferIterableWhereType()) + ..register(PreferMixin()) + ..register(PreferNullAwareMethodCalls()) + ..register(PreferNullAwareOperators()) + ..register(PreferRelativeImports()) + ..register(PreferSingleQuotes()) + ..register(PreferSpreadCollections()) + ..register(PreferTypingUninitializedVariables()) + ..register(PreferVoidToNull()) + ..register(ProvideDeprecationMessage()) + ..register(PublicMemberApiDocs()) + ..register(RecursiveGetters()) + ..register(RequireTrailingCommas()) + ..register(SecurePubspecUrls()) + ..register(SizedBoxForWhitespace()) + ..register(SizedBoxShrinkExpand()) + ..register(SlashForDocComments()) + ..register(SortChildPropertiesLast()) + ..register(SortConstructorsFirst()) + ..register(SortPubDependencies()) + ..register(SortUnnamedConstructorsFirst()) + ..register(SuperGoesLast()) + ..register(TestTypesInEquals()) + ..register(ThrowInFinally()) + ..register(TightenTypeOfInitializingFormals()) + ..register(TypeAnnotatePublicApis()) + ..register(TypeInitFormals()) + ..register(TypeLiteralInConstantPattern()) + ..register(UnawaitedFutures()) + ..register(UnnecessaryAwaitInReturn()) + ..register(UnnecessaryBraceInStringInterps()) + ..register(UnnecessaryBreaks()) + ..register(UnnecessaryConst()) + ..register(UnnecessaryConstructorName()) + ..register(UnnecessaryFinal()) + ..register(UnnecessaryGettersSetters()) + ..register(UnnecessaryLambdas()) + ..register(UnnecessaryLate()) + ..register(UnnecessaryLibraryDirective()) + ..register(UnnecessaryNew()) + ..register(UnnecessaryNullAwareAssignments()) + ..register(UnnecessaryNullAwareOperatorOnExtensionOnNullable()) + ..register(UnnecessaryNullChecks()) + ..register(UnnecessaryNullInIfNullOperators()) + ..register(UnnecessaryNullableForFinalVariableDeclarations()) + ..register(UnnecessaryOverrides()) + ..register(UnnecessaryParenthesis()) + ..register(UnnecessaryRawStrings()) + ..register(UnnecessaryStatements()) + ..register(UnnecessaryStringEscapes()) + ..register(UnnecessaryStringInterpolations()) + ..register(UnnecessaryThis()) + ..register(UnnecessaryToListInSpreads()) + ..register(UnreachableFromMain()) + ..register(UnrelatedTypeEqualityChecks()) + ..register(UnsafeHtml()) + ..register(UseBuildContextSynchronously(inTestMode: inTestMode)) + ..register(UseColoredBox()) + ..register(UseDecoratedBox()) + ..register(UseEnums()) + ..register(UseFullHexValuesForFlutterColors()) + ..register(UseFunctionTypeSyntaxForParameters()) + ..register(UseIfNullToConvertNullsToBools()) + ..register(UseIsEvenRatherThanModulo()) + ..register(UseKeyInWidgetConstructors()) + ..register(UseLateForPrivateFieldsAndVariables()) + ..register(UseNamedConstants()) + ..register(UseRawStrings()) + ..register(UseRethrowWhenPossible()) + ..register(UseSettersToChangeProperties()) + ..register(UseStringBuffers()) + ..register(UseStringInPartOfDirectives()) + ..register(UseSuperParameters()) + ..register(UseTestThrowsMatchers()) + ..register(UseToAndAsIfApplicable()) + ..register(ValidRegexps()) + ..register(VoidChecks()); +} diff --git a/pkg/linter/lib/src/rules/always_declare_return_types.dart b/pkg/linter/lib/src/rules/always_declare_return_types.dart new file mode 100644 index 00000000000..58416d6eecb --- /dev/null +++ b/pkg/linter/lib/src/rules/always_declare_return_types.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Declare method return types.'; + +const _details = r''' +**DO** declare method return types. + +When declaring a method or function *always* specify a return type. +Declaring return types for functions helps improve your codebase by allowing the +analyzer to more adequately check your code for errors that could occur during +runtime. + +**BAD:** +```dart +main() { } + +_bar() => _Foo(); + +class _Foo { + _foo() => 42; +} +``` + +**GOOD:** +```dart +void main() { } + +_Foo _bar() => _Foo(); + +class _Foo { + int _foo() => 42; +} + +typedef predicate = bool Function(Object o); +``` + +'''; + +class AlwaysDeclareReturnTypes extends LintRule { + static const LintCode code = LintCode( + 'always_declare_return_types', 'Missing return type on method.', + correctionMessage: 'Try adding a return type.'); + + AlwaysDeclareReturnTypes() + : super( + name: 'always_declare_return_types', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclaration(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + static const LintCode functionCode = LintCode( + "always_declare_return_types", // ignore: prefer_single_quotes + "The function '{0}' should have a return type but doesn't.", + correctionMessage: + "Try adding a return type to the function."); // ignore: prefer_single_quotes + + static const LintCode methodCode = LintCode( + "always_declare_return_types", // ignore: prefer_single_quotes + "The method '{0}' should have a return type but doesn't.", + correctionMessage: + "Try adding a return type to the method."); // ignore: prefer_single_quotes + + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + if (!node.isSetter && node.returnType == null) { + rule.reportLintForToken(node.name, + arguments: [node.name.lexeme], errorCode: functionCode); + } + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + if (node.returnType == null) { + rule.reportLintForToken(node.name, + arguments: [node.name.lexeme], errorCode: functionCode); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (!node.isSetter && + node.returnType == null && + node.name.type != TokenType.INDEX_EQ) { + rule.reportLintForToken(node.name, + arguments: [node.name.lexeme], errorCode: methodCode); + } + } +} diff --git a/pkg/linter/lib/src/rules/always_put_control_body_on_new_line.dart b/pkg/linter/lib/src/rules/always_put_control_body_on_new_line.dart new file mode 100644 index 00000000000..8e9cc7fde8f --- /dev/null +++ b/pkg/linter/lib/src/rules/always_put_control_body_on_new_line.dart @@ -0,0 +1,120 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Separate the control structure expression from its statement.'; + +const _details = r''' +From the [style guide for the flutter repo](https://flutter.dev/style-guide/): + +**DO** separate the control structure expression from its statement. + +Don't put the statement part of an `if`, `for`, `while`, `do` on the same line +as the expression, even if it is short. Doing so makes it unclear that there +is relevant code there. This is especially important for early returns. + +**BAD:** +```dart +if (notReady) return; + +if (notReady) + return; +else print('ok') + +while (condition) i += 1; +``` + +**GOOD:** +```dart +if (notReady) + return; + +if (notReady) + return; +else + print('ok') + +while (condition) + i += 1; +``` + +Note that this rule can conflict with the +[Dart formatter](https://dart.dev/tools/dart-format), and should not be enabled +when the Dart formatter is used. + +'''; + +class AlwaysPutControlBodyOnNewLine extends LintRule { + static const LintCode code = LintCode('always_put_control_body_on_new_line', + 'Statement should be on a separate line.', + correctionMessage: 'Try moving the statement to a new line.'); + + AlwaysPutControlBodyOnNewLine() + : super( + name: 'always_put_control_body_on_new_line', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDoStatement(this, visitor); + registry.addForStatement(this, visitor); + registry.addIfStatement(this, visitor); + registry.addWhileStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitDoStatement(DoStatement node) { + _checkNodeOnNextLine(node.body, node.doKeyword.end); + } + + @override + void visitForStatement(ForStatement node) { + _checkNodeOnNextLine(node.body, node.rightParenthesis.end); + } + + @override + void visitIfStatement(IfStatement node) { + _checkNodeOnNextLine(node.thenStatement, node.rightParenthesis.end); + var elseKeyword = node.elseKeyword; + var elseStatement = node.elseStatement; + if (elseKeyword != null && elseStatement is! IfStatement) { + _checkNodeOnNextLine(elseStatement, elseKeyword.end); + } + } + + @override + void visitWhileStatement(WhileStatement node) { + _checkNodeOnNextLine(node.body, node.rightParenthesis.end); + } + + void _checkNodeOnNextLine(AstNode? node, int controlEnd) { + if (node == null || node is Block && node.statements.isEmpty) return; + + var unit = node.root as CompilationUnit; + var offsetFirstStatement = + node is Block ? node.statements.first.offset : node.offset; + var lineInfo = unit.lineInfo; + if (lineInfo.getLocation(controlEnd).lineNumber == + lineInfo.getLocation(offsetFirstStatement).lineNumber) { + rule.reportLintForToken(node.beginToken); + } + } +} diff --git a/pkg/linter/lib/src/rules/always_put_required_named_parameters_first.dart b/pkg/linter/lib/src/rules/always_put_required_named_parameters_first.dart new file mode 100644 index 00000000000..1df7aaa102a --- /dev/null +++ b/pkg/linter/lib/src/rules/always_put_required_named_parameters_first.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Put required named parameters first.'; + +const _details = r''' +**DO** specify `required` on named parameter before other named parameters. + +**BAD:** +```dart +m({b, c, required a}) ; +``` + +**GOOD:** +```dart +m({required a, b, c}) ; +``` + +**BAD:** +```dart +m({b, c, @required a}) ; +``` + +**GOOD:** +```dart +m({@required a, b, c}) ; +``` + +'''; + +class AlwaysPutRequiredNamedParametersFirst extends LintRule { + static const LintCode code = LintCode( + 'always_put_required_named_parameters_first', + 'Required named parameters should be before optional named parameters.', + correctionMessage: + 'Try moving the required named parameter to be before any optional ' + 'named parameters.'); + + AlwaysPutRequiredNamedParametersFirst() + : super( + name: 'always_put_required_named_parameters_first', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFormalParameterList(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFormalParameterList(FormalParameterList node) { + var nonRequiredSeen = false; + for (var param in node.parameters.where((p) => p.isNamed)) { + var element = param.declaredElement; + if (element != null && (element.hasRequired || element.isRequiredNamed)) { + if (nonRequiredSeen) { + var name = param.name; + if (name != null) { + rule.reportLintForToken(name); + } + } + } else { + nonRequiredSeen = true; + } + } + } +} diff --git a/pkg/linter/lib/src/rules/always_require_non_null_named_parameters.dart b/pkg/linter/lib/src/rules/always_require_non_null_named_parameters.dart new file mode 100644 index 00000000000..bffe12da1f5 --- /dev/null +++ b/pkg/linter/lib/src/rules/always_require_non_null_named_parameters.dart @@ -0,0 +1,160 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Specify `@required` on named parameters without defaults.'; + +const _details = r''' +**DO** specify `@required` on named parameters without a default value on which +an `assert(param != null)` is done. + +**BAD:** +```dart +m1({a}) { + assert(a != null); +} +``` + +**GOOD:** +```dart +m1({@required a}) { + assert(a != null); +} + +m2({a: 1}) { + assert(a != null); +} +``` + +NOTE: Only asserts at the start of the bodies will be taken into account. + +'''; + +class AlwaysRequireNonNullNamedParameters extends LintRule { + static const LintCode code = LintCode( + 'always_require_non_null_named_parameters', + "Named parameters without a default value should be annotated with '@required'.", + correctionMessage: "Try adding the '@required' annotation."); + + AlwaysRequireNonNullNamedParameters() + : super( + name: 'always_require_non_null_named_parameters', + description: _desc, + details: _details, + state: State.deprecated(since: dart2_12), + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + // In a Null Safety library, this lint is covered by other formal static + // analysis. + if (!context.isEnabled(Feature.non_nullable)) { + var visitor = _Visitor(this); + registry.addFormalParameterList(this, visitor); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFormalParameterList(FormalParameterList node) { + List getParams() { + var params = []; + for (var p in node.parameters) { + // Only named parameters + if (p.isNamed) { + var parameter = p as DefaultFormalParameter; + // Without a default value or marked required + if (parameter.defaultValue == null) { + var declaredElement = parameter.declaredElement; + if (declaredElement != null && !declaredElement.hasRequired) { + params.add(parameter); + } + } + } + } + return params; + } + + var parent = node.parent; + if (parent is FunctionExpression) { + _checkParams(getParams(), parent.body); + } else if (parent is ConstructorDeclaration) { + _checkInitializerList(getParams(), parent.initializers); + _checkParams(getParams(), parent.body); + } else if (parent is MethodDeclaration) { + _checkParams(getParams(), parent.body); + } + } + + void _checkAssert( + Expression assertExpression, List params) { + for (var param in params) { + var name = param.name; + if (name != null && _hasAssertNotNull(assertExpression, name.lexeme)) { + rule.reportLintForToken(name); + params.remove(param); + return; + } + } + } + + void _checkInitializerList(List params, + NodeList initializers) { + for (var initializer in initializers) { + if (initializer is AssertInitializer) { + _checkAssert(initializer.condition, params); + } + } + } + + void _checkParams(List params, FunctionBody body) { + if (body is BlockFunctionBody) { + for (var statement in body.block.statements) { + if (statement is AssertStatement) { + _checkAssert(statement.condition, params); + } else { + // Bail on first non-assert. + return; + } + } + } + } + + bool _hasAssertNotNull(Expression node, String name) { + bool hasSameName(Expression rawExpression) { + var expression = rawExpression.unParenthesized; + return expression is SimpleIdentifier && expression.name == name; + } + + var expression = node.unParenthesized; + if (expression is BinaryExpression) { + if (expression.operator.type == TokenType.AMPERSAND_AMPERSAND) { + return _hasAssertNotNull(expression.leftOperand, name) || + _hasAssertNotNull(expression.rightOperand, name); + } + if (expression.operator.type == TokenType.BANG_EQ) { + var operands = [expression.leftOperand, expression.rightOperand]; + return operands.any((e) => e.isNullLiteral) && + operands.any(hasSameName); + } + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/always_specify_types.dart b/pkg/linter/lib/src/rules/always_specify_types.dart new file mode 100644 index 00000000000..5f3814544b4 --- /dev/null +++ b/pkg/linter/lib/src/rules/always_specify_types.dart @@ -0,0 +1,278 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Specify type annotations.'; + +const _details = r''' +From the [style guide for the flutter repo](https://flutter.dev/style-guide/): + +**DO** specify type annotations. + +Avoid `var` when specifying that a type is unknown and short-hands that elide +type annotations. Use `dynamic` if you are being explicit that the type is +unknown. Use `Object` if you are being explicit that you want an object that +implements `==` and `hashCode`. + +**BAD:** +```dart +var foo = 10; +final bar = Bar(); +const quux = 20; +``` + +**GOOD:** +```dart +int foo = 10; +final Bar bar = Bar(); +String baz = 'hello'; +const int quux = 20; +``` + +NOTE: Using the the `@optionalTypeArgs` annotation in the `meta` package, API +authors can special-case type variables whose type needs to by dynamic but whose +declaration should be treated as optional. For example, suppose you have a +`Key` object whose type parameter you'd like to treat as optional. Using the +`@optionalTypeArgs` would look like this: + +```dart +import 'package:meta/meta.dart'; + +@optionalTypeArgs +class Key { + ... +} + +main() { + Key s = Key(); // OK! +} +``` + +'''; + +class AlwaysSpecifyTypes extends LintRule { + static const LintCode code = LintCode( + 'always_specify_types', 'Missing type annotation.', + correctionMessage: 'Try adding a type annotation.'); + + AlwaysSpecifyTypes() + : super( + name: 'always_specify_types', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => + const ['avoid_types_on_closure_parameters', 'omit_local_variable_types']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDeclaredIdentifier(this, visitor); + registry.addListLiteral(this, visitor); + registry.addSetOrMapLiteral(this, visitor); + registry.addSimpleFormalParameter(this, visitor); + registry.addNamedType(this, visitor); + registry.addDeclaredVariablePattern(this, visitor); + registry.addVariableDeclarationList(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + static const LintCode keywordCouldBeTypeCode = LintCode( + 'always_specify_types', + "Missing type annotation.", // ignore: prefer_single_quotes + correctionMessage: "Try replacing '{0}' with '{1}'."); + + static const LintCode keywordCouldBeSplitToTypesCode = LintCode( + 'always_specify_types', + "Missing type annotation.", // ignore: prefer_single_quotes + correctionMessage: + "Try splitting the declaration and specify the different type annotations."); // ignore: prefer_single_quotes + + static const LintCode specifyTypeCode = LintCode('always_specify_types', + "Missing type annotation.", // ignore: prefer_single_quotes + correctionMessage: "Try specifying the type '{0}'."); + + final LintRule rule; + + _Visitor(this.rule); + + void checkLiteral(TypedLiteral literal) { + if (literal.typeArguments == null) { + rule.reportLintForToken(literal.beginToken); + } + } + + @override + void visitDeclaredIdentifier(DeclaredIdentifier node) { + var keyword = node.keyword; + if (node.type == null && keyword != null) { + var element = node.declaredElement; + if (element is VariableElement) { + if (keyword.keyword == Keyword.VAR) { + rule.reportLintForToken(keyword, + arguments: [keyword.lexeme, element!.type], + errorCode: keywordCouldBeTypeCode); + } else { + rule.reportLintForToken(keyword, + arguments: [element!.type], errorCode: specifyTypeCode); + } + } + } + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + if (node.type == null) { + var type = node.matchedValueType!; + var keyword = node.keyword; + var tokenToLint = keyword ?? node.name; + if (keyword != null && keyword.keyword == Keyword.VAR) { + rule.reportLintForToken(tokenToLint, + arguments: [keyword.lexeme, type], + errorCode: keywordCouldBeTypeCode); + } else { + rule.reportLintForToken(tokenToLint, + arguments: [type], errorCode: specifyTypeCode); + } + } + } + + @override + void visitListLiteral(ListLiteral literal) { + checkLiteral(literal); + } + + @override + void visitNamedType(NamedType namedType) { + var type = namedType.type; + if (type is InterfaceType) { + var element = namedType.element; + if (element is TypeParameterizedElement && + element.typeParameters.isNotEmpty && + namedType.typeArguments == null && + namedType.parent is! IsExpression && + !element.hasOptionalTypeArgs) { + rule.reportLint(namedType); + } + } + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral literal) { + checkLiteral(literal); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter param) { + var name = param.name; + if (name != null && param.type == null && !name.lexeme.isJustUnderscores) { + var keyword = param.keyword; + if (keyword != null) { + var type = param.declaredElement?.type; + + if (keyword.type == Keyword.VAR && + type != null && + type is! DynamicType) { + rule.reportLintForToken(keyword, + arguments: [keyword.lexeme, type], + errorCode: keywordCouldBeTypeCode); + } else { + rule.reportLintForToken(keyword); + } + } else if (param.declaredElement != null) { + var type = param.declaredElement!.type; + + if (type is DynamicType) { + rule.reportLint(param); + } else { + rule.reportLint(param, arguments: [type], errorCode: specifyTypeCode); + } + } + } + } + + @override + void visitVariableDeclarationList(VariableDeclarationList list) { + var keyword = list.keyword; + if (list.type == null && keyword != null) { + Set types; + var parent = list.parent; + if (parent is TopLevelVariableDeclaration) { + types = _getTypes(parent.variables); + } else if (parent is ForPartsWithDeclarations) { + types = _getTypes(parent.variables); + } else if (parent is FieldDeclaration) { + types = _getTypes(parent.fields); + } else if (parent is VariableDeclarationStatement) { + types = _getTypes(parent.variables); + } else { + return; + } + + var singleType = types.length == 1; + + List arguments; + ErrorCode? errorCode; + if (types.isEmpty) { + arguments = []; + } else if (keyword.type == Keyword.VAR) { + if (singleType) { + arguments = [keyword.lexeme, types.first]; + errorCode = keywordCouldBeTypeCode; + } else { + arguments = []; + errorCode = keywordCouldBeSplitToTypesCode; + } + } else { + if (singleType) { + arguments = [types.first]; + errorCode = specifyTypeCode; + } else { + arguments = []; + } + } + rule.reportLintForToken(keyword, + arguments: arguments, errorCode: errorCode); + } + } + + Set _getTypes(VariableDeclarationList list) { + var types = {}; + for (var variable in list.variables) { + var initializer = variable.initializer; + if (initializer != null) { + DartType? type; + if (initializer is Identifier) { + var staticElement = initializer.staticElement; + if (staticElement is VariableElement) { + type = staticElement.type; + } + } + + type ??= variable.initializer?.staticType; + + if (type != null) { + types.add(type.getDisplayString(withNullability: true)); + } + } + } + return types; + } +} diff --git a/pkg/linter/lib/src/rules/always_use_package_imports.dart b/pkg/linter/lib/src/rules/always_use_package_imports.dart new file mode 100644 index 00000000000..b802b330ade --- /dev/null +++ b/pkg/linter/lib/src/rules/always_use_package_imports.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Avoid relative imports for files in `lib/`.'; + +const _details = r''' +**DO** avoid relative imports for files in `lib/`. + +When mixing relative and absolute imports it's possible to create confusion +where the same member gets imported in two different ways. One way to avoid +that is to ensure you consistently use absolute imports for files within the +`lib/` directory. + +This is the opposite of 'prefer_relative_imports'. + +You can also use 'avoid_relative_lib_imports' to disallow relative imports of +files within `lib/` directory outside of it (for example `test/`). + +**BAD:** +```dart +import 'baz.dart'; + +import 'src/bag.dart' + +import '../lib/baz.dart'; + +... +``` + +**GOOD:** +```dart +import 'package:foo/bar.dart'; + +import 'package:foo/baz.dart'; + +import 'package:foo/src/baz.dart'; +... +``` + +'''; + +class AlwaysUsePackageImports extends LintRule { + static const LintCode code = LintCode('always_use_package_imports', + "Use 'package:' imports for files in the 'lib' directory.", + correctionMessage: "Try converting the URI to a 'package:' URI."); + + AlwaysUsePackageImports() + : super( + name: 'always_use_package_imports', + description: _desc, + details: _details, + group: Group.errors); + + @override + List get incompatibleRules => const ['prefer_relative_imports']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + // Relative paths from outside of the lib folder are handled by the + // `avoid_relative_lib_imports` lint. + if (!isInLibDir(context.currentUnit.unit, context.package)) { + return; + } + + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isRelativeImport(ImportDirective node) { + var uriContent = node.uri.stringValue; + if (uriContent != null) { + var uri = Uri.tryParse(uriContent); + return uri != null && uri.scheme.isEmpty; + } + return false; + } + + @override + void visitImportDirective(ImportDirective node) { + if (isRelativeImport(node)) { + rule.reportLint(node.uri); + } + } +} diff --git a/pkg/linter/lib/src/rules/annotate_overrides.dart b/pkg/linter/lib/src/rules/annotate_overrides.dart new file mode 100644 index 00000000000..5cf064e3961 --- /dev/null +++ b/pkg/linter/lib/src/rules/annotate_overrides.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Annotate overridden members.'; + +const _details = r''' +**DO** annotate overridden methods and fields. + +This practice improves code readability and helps protect against +unintentionally overriding superclass members. + +**BAD:** +```dart +class Cat { + int get lives => 9; +} + +class Lucky extends Cat { + final int lives = 14; +} +``` + +**GOOD:** +```dart +abstract class Dog { + String get breed; + void bark() {} +} + +class Husky extends Dog { + @override + final String breed = 'Husky'; + @override + void bark() {} +} +``` + +'''; + +class AnnotateOverrides extends LintRule { + static const LintCode code = LintCode( + 'annotate_overrides', + "The member '{0}' overrides an inherited member but isn't annotated " + "with '@override'.", + correctionMessage: "Try adding the '@override' annotation."); + + AnnotateOverrides() + : super( + name: 'annotate_overrides', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addFieldDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + void check(Element? element, Token target) { + if (element == null || element.hasOverride) return; + + var member = context.inheritanceManager.overriddenMember(element); + if (member != null) { + rule.reportLintForToken(target, arguments: [member.name]); + } + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isStatic) return; + + for (var field in node.fields.variables) { + check(field.declaredElement, field.name); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isStatic) return; + + check(node.declaredElement, node.name); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_annotating_with_dynamic.dart b/pkg/linter/lib/src/rules/avoid_annotating_with_dynamic.dart new file mode 100644 index 00000000000..1ef57123413 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_annotating_with_dynamic.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid annotating with dynamic when not required.'; + +const _details = r''' +**AVOID** annotating with dynamic when not required. + +As `dynamic` is the assumed return value of a function or method, it is usually +not necessary to annotate it. + +**BAD:** +```dart +dynamic lookUpOrDefault(String name, Map map, dynamic defaultValue) { + var value = map[name]; + if (value != null) return value; + return defaultValue; +} +``` + +**GOOD:** +```dart +lookUpOrDefault(String name, Map map, defaultValue) { + var value = map[name]; + if (value != null) return value; + return defaultValue; +} +``` + +'''; + +class AvoidAnnotatingWithDynamic extends LintRule { + static const LintCode code = LintCode( + 'avoid_annotating_with_dynamic', "Unnecessary 'dynamic' type annotation.", + correctionMessage: "Try removing the type 'dynamic'."); + + AvoidAnnotatingWithDynamic() + : super( + name: 'avoid_annotating_with_dynamic', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldFormalParameter(this, visitor); + registry.addSimpleFormalParameter(this, visitor); + registry.addSuperFormalParameter(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + _checkNode(node, node.type); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter node) { + _checkNode(node, node.type); + } + + @override + void visitSuperFormalParameter(SuperFormalParameter node) { + _checkNode(node, node.type); + } + + void _checkNode(NormalFormalParameter node, TypeAnnotation? type) { + if (type is NamedType && type.type is DynamicType) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_as.dart b/pkg/linter/lib/src/rules/avoid_as.dart new file mode 100644 index 00000000000..ced967b2549 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_as.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = r'Avoid using `as`.'; + +const _details = r''' +**AVOID** using `as`. + +If you know the type is correct, use an assertion or assign to a more +narrowly-typed variable (this avoids the type check in release mode; `as` is not +compiled out in release mode). If you don't know whether the type is +correct, check using `is` (this avoids the exception that `as` raises). + +**BAD:** +```dart +(pm as Person).firstName = 'Seth'; +``` + +**GOOD:** +```dart +if (pm is Person) + pm.firstName = 'Seth'; +``` + +but certainly not + +**BAD:** +```dart +try { + (pm as Person).firstName = 'Seth'; +} on CastError { } +``` + +Note that an exception is made in the case of `dynamic` since the cast has no +performance impact. + +**OK:** +```dart +HasScrollDirection scrollable = renderObject as dynamic; +``` +'''; + +class AvoidAs extends LintRule { + static const LintCode code = LintCode('avoid_as', "Unnecessary use of 'as'.", + correctionMessage: "Try adding an explicit type check ('is')."); + + AvoidAs() + : super( + name: 'avoid_as', + description: _desc, + details: _details, + group: Group.style, + state: State.removed(since: dart2_12), + ); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/avoid_bool_literals_in_conditional_expressions.dart b/pkg/linter/lib/src/rules/avoid_bool_literals_in_conditional_expressions.dart new file mode 100644 index 00000000000..58982a44536 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_bool_literals_in_conditional_expressions.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid bool literals in conditional expressions.'; + +const _details = r''' +**AVOID** bool literals in conditional expressions. + +**BAD:** +```dart +condition ? true : boolExpression +condition ? false : boolExpression +condition ? boolExpression : true +condition ? boolExpression : false +``` + +**GOOD:** +```dart +condition || boolExpression +!condition && boolExpression +!condition || boolExpression +condition && boolExpression +``` + +'''; + +class AvoidBoolLiteralsInConditionalExpressions extends LintRule { + static const LintCode code = LintCode( + 'avoid_bool_literals_in_conditional_expressions', + "Conditional expressions with a 'bool' literal can be simplified.", + correctionMessage: + "Try rewriting the expression to use either '&&' or '||'."); + + AvoidBoolLiteralsInConditionalExpressions() + : super( + name: 'avoid_bool_literals_in_conditional_expressions', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addConditionalExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitConditionalExpression(ConditionalExpression node) { + var typeProvider = context.typeProvider; + var thenExp = node.thenExpression; + var elseExp = node.elseExpression; + + if (thenExp.staticType == typeProvider.boolType && + elseExp.staticType == typeProvider.boolType) { + if (thenExp is BooleanLiteral) rule.reportLint(node); + if (elseExp is BooleanLiteral) rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_catches_without_on_clauses.dart b/pkg/linter/lib/src/rules/avoid_catches_without_on_clauses.dart new file mode 100644 index 00000000000..3179799ae54 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_catches_without_on_clauses.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid catches without on clauses.'; + +const _details = r''' +**AVOID** catches without on clauses. + +Using catch clauses without on clauses make your code prone to encountering +unexpected errors that won't be thrown (and thus will go unnoticed). + +**BAD:** +```dart +try { + somethingRisky() +} +catch(e) { + doSomething(e); +} +``` + +**GOOD:** +```dart +try { + somethingRisky() +} +on Exception catch(e) { + doSomething(e); +} +``` + +'''; + +class AvoidCatchesWithoutOnClauses extends LintRule { + static const LintCode code = LintCode( + 'avoid_catches_without_on_clauses', + "Catch clause needs to use 'on' to specify the type of exception being " + 'caught.', + correctionMessage: "Try adding an 'on' clause before the 'catch'."); + + AvoidCatchesWithoutOnClauses() + : super( + name: 'avoid_catches_without_on_clauses', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCatchClause(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitCatchClause(CatchClause node) { + if (node.onKeyword == null) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_catching_errors.dart b/pkg/linter/lib/src/rules/avoid_catching_errors.dart new file mode 100644 index 00000000000..44c01b86735 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_catching_errors.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r"Don't explicitly catch Error or types that implement it."; + +const _details = r''' +**DON'T** explicitly catch Error or types that implement it. + +Errors differ from Exceptions in that Errors can be analyzed and prevented prior +to runtime. It should almost never be necessary to catch an error at runtime. + +**BAD:** +```dart +try { + somethingRisky(); +} on Error catch(e) { + doSomething(e); +} +``` + +**GOOD:** +```dart +try { + somethingRisky(); +} on Exception catch(e) { + doSomething(e); +} +``` + +'''; + +class AvoidCatchingErrors extends LintRule { + static const LintCode classCode = LintCode( + 'avoid_catching_errors', "The type 'Error' should not be caught.", + uniqueName: 'LintCode.avoid_catching_errors_class'); + + static const LintCode subclassCode = LintCode('avoid_catching_errors', + "The type '{0}' should not be caught because it is a subclass of 'Error'.", + uniqueName: 'LintCode.avoid_catching_errors_subclass'); + + AvoidCatchingErrors() + : super( + name: 'avoid_catching_errors', + description: _desc, + details: _details, + group: Group.style); + + @override + List get lintCodes => [classCode, subclassCode]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCatchClause(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitCatchClause(CatchClause node) { + var exceptionType = node.exceptionType?.type; + if (exceptionType.implementsInterface('Error', 'dart.core')) { + if (exceptionType.isSameAs('Error', 'dart.core')) { + rule.reportLint(node, errorCode: AvoidCatchingErrors.classCode); + } else { + rule.reportLint(node, + errorCode: AvoidCatchingErrors.subclassCode, + arguments: [ + exceptionType!.getDisplayString(withNullability: false) + ]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_classes_with_only_static_members.dart b/pkg/linter/lib/src/rules/avoid_classes_with_only_static_members.dart new file mode 100644 index 00000000000..b5d71791545 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_classes_with_only_static_members.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid defining a class that contains only static members.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#avoid-defining-a-class-that-contains-only-static-members): + +**AVOID** defining a class that contains only static members. + +Creating classes with the sole purpose of providing utility or otherwise static +methods is discouraged. Dart allows functions to exist outside of classes for +this very reason. + +**BAD:** +```dart +class DateUtils { + static DateTime mostRecent(List dates) { + return dates.reduce((a, b) => a.isAfter(b) ? a : b); + } +} + +class _Favorites { + static const mammal = 'weasel'; +} +``` + +**GOOD:** +```dart +DateTime mostRecent(List dates) { + return dates.reduce((a, b) => a.isAfter(b) ? a : b); +} + +const _favoriteMammal = 'weasel'; +``` + +'''; + +bool _isNonConst(FieldElement element) => !element.isConst; + +bool _isStaticMember(ClassMember classMember) { + if (classMember is MethodDeclaration) { + return classMember.isStatic; + } + if (classMember is FieldDeclaration) { + return classMember.isStatic; + } + return false; +} + +class AvoidClassesWithOnlyStaticMembers extends LintRule { + static const LintCode code = LintCode( + 'avoid_classes_with_only_static_members', + 'Classes should define instance members.', + correctionMessage: + 'Try adding instance behavior or moving the members out of the class.'); + + AvoidClassesWithOnlyStaticMembers() + : super( + name: 'avoid_classes_with_only_static_members', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitClassDeclaration(ClassDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement == null) { + return; + } + + var interface = context.inheritanceManager.getInterface(declaredElement); + var map = interface.map; + for (var member in map.values) { + var enclosingElement = member.enclosingElement; + if (enclosingElement is ClassElement && + !enclosingElement.isDartCoreObject) { + return; + } + } + + if (node.members.isNotEmpty && + node.members.every(_isStaticMember) && + (declaredElement.methods.isNotEmpty || + declaredElement.fields.any(_isNonConst))) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_double_and_int_checks.dart b/pkg/linter/lib/src/rules/avoid_double_and_int_checks.dart new file mode 100644 index 00000000000..27717244a15 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_double_and_int_checks.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid double and int checks.'; + +const _details = r''' +**AVOID** to check if type is double or int. + +When compiled to JS, integer values are represented as floats. That can lead to +some unexpected behavior when using either `is` or `is!` where the type is +either `int` or `double`. + +**BAD:** +```dart +f(num x) { + if (x is double) { + ... + } else if (x is int) { + ... + } +} +``` + +**GOOD:** +```dart +f(dynamic x) { + if (x is num) { + ... + } else { + ... + } +} +``` + +'''; + +class AvoidDoubleAndIntChecks extends LintRule { + static const LintCode code = LintCode( + 'avoid_double_and_int_checks', 'Explicit check for double or int.', + correctionMessage: 'Try removing the check.'); + + AvoidDoubleAndIntChecks() + : super( + name: 'avoid_double_and_int_checks', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addIfStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitIfStatement(IfStatement node) { + var elseStatement = node.elseStatement; + if (elseStatement is IfStatement) { + var ifCondition = node.expression; + var elseCondition = elseStatement.expression; + if (ifCondition is IsExpression && elseCondition is IsExpression) { + var typeProvider = context.typeProvider; + var ifExpression = ifCondition.expression; + var elseIsExpression = elseCondition.expression; + if (ifExpression is SimpleIdentifier && + elseIsExpression is SimpleIdentifier && + ifExpression.name == elseIsExpression.name && + ifCondition.type.type == typeProvider.doubleType && + elseCondition.type.type == typeProvider.intType && + (ifExpression.staticElement is ParameterElement || + ifExpression.staticElement is LocalVariableElement)) { + rule.reportLint(elseCondition); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart b/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart new file mode 100644 index 00000000000..3218ccb299c --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart @@ -0,0 +1,281 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid method calls or property accesses on a "dynamic" target.'; + +const _details = r''' +**DO** avoid method calls or accessing properties on an object that is either +explicitly or implicitly statically typed "dynamic". Dynamic calls are treated +slightly different in every runtime environment and compiler, but most +production modes (and even some development modes) have both compile size and +runtime performance penalties associated with dynamic calls. + +Additionally, targets typed "dynamic" disables most static analysis, meaning it +is easier to lead to a runtime "NoSuchMethodError" or "NullError" than properly +statically typed Dart code. + +There is an exception to methods and properties that exist on "Object?": +- a.hashCode +- a.runtimeType +- a.noSuchMethod(someInvocation) +- a.toString() + +... these members are dynamically dispatched in the web-based runtimes, but not +in the VM-based ones. Additionally, they are so common that it would be very +punishing to disallow `any.toString()` or `any == true`, for example. + +Note that despite "Function" being a type, the semantics are close to identical +to "dynamic", and calls to an object that is typed "Function" will also trigger +this lint. + +**BAD:** +```dart +void explicitDynamicType(dynamic object) { + print(object.foo()); +} + +void implicitDynamicType(object) { + print(object.foo()); +} + +abstract class SomeWrapper { + T doSomething(); +} + +void inferredDynamicType(SomeWrapper wrapper) { + var object = wrapper.doSomething(); + print(object.foo()); +} + +void callDynamic(dynamic function) { + function(); +} + +void functionType(Function function) { + function(); +} +``` + +**GOOD:** +```dart +void explicitType(Fooable object) { + object.foo(); +} + +void castedType(dynamic object) { + (object as Fooable).foo(); +} + +abstract class SomeWrapper { + T doSomething(); +} + +void inferredType(SomeWrapper wrapper) { + var object = wrapper.doSomething(); + object.foo(); +} + +void functionTypeWithParameters(Function() function) { + function(); +} +``` + +'''; + +class AvoidDynamicCalls extends LintRule { + static const LintCode code = LintCode('avoid_dynamic_calls', + "Method invocation or property access on a 'dynamic' target.", + correctionMessage: 'Try giving the target a type.'); + + AvoidDynamicCalls() + : super( + name: 'avoid_dynamic_calls', + description: _desc, + details: _details, + group: Group.errors, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this); + registry + ..addAssignmentExpression(this, visitor) + ..addBinaryExpression(this, visitor) + ..addFunctionExpressionInvocation(this, visitor) + ..addIndexExpression(this, visitor) + ..addMethodInvocation(this, visitor) + ..addPostfixExpression(this, visitor) + ..addPrefixExpression(this, visitor) + ..addPrefixedIdentifier(this, visitor) + ..addPropertyAccess(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.readType is! DynamicType) { + // An assignment expression can only be a dynamic call if it is a + // "compound assignment" (i.e. such as `x += 1`); so if `readType` is not + // dynamic, we don't need to check further. + return; + } + if (node.operator.type == TokenType.QUESTION_QUESTION_EQ) { + // x ??= foo is not a dynamic call. + return; + } + rule.reportLint(node); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + if (!node.operator.isUserDefinableOperator) { + // Operators that can never be provided by the user can't be dynamic. + return; + } + switch (node.operator.type) { + case TokenType.EQ_EQ: + case TokenType.BANG_EQ: + // These operators exist on every type, even "Object?". While they are + // virtually dispatched, they are not considered dynamic calls by the + // CFE. They would also make landing this lint exponentially harder. + return; + } + _lintIfDynamic(node.leftOperand); + // We don't check node.rightOperand, because that is an implicit cast, not a + // dynamic call (the call itself is based on leftOperand). While it would be + // useful to do so, it is better solved by other more specific lints to + // disallow implicit casts from dynamic. + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + _lintIfDynamicOrFunction(node.function); + } + + @override + void visitIndexExpression(IndexExpression node) { + _lintIfDynamic(node.realTarget); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var methodName = node.methodName.name; + if (node.target != null) { + if (methodName == 'noSuchMethod' && + node.argumentList.arguments.length == 1 && + node.argumentList.arguments.first is! NamedExpression) { + // Special-cased; these exist on every object, even those typed "Object?". + return; + } + if (methodName == 'toString' && node.argumentList.arguments.isEmpty) { + // Special-cased; these exist on every object, even those typed "Object?". + return; + } + } + var receiverWasDynamic = _lintIfDynamic(node.realTarget); + if (!receiverWasDynamic) { + var target = node.target; + // The ".call" method is special, where "a.call()" is treated ~as "a()". + // + // If the method is "call", and the receiver is a function, we assume then + // we are really checking the static type of the receiver, not the static + // type of the "call" method itself. + DartType? staticType; + if (methodName == 'call' && + target != null && + target.staticType is FunctionType) { + staticType = target.staticType; + } + _lintIfDynamicOrFunction(node.function, staticType: staticType); + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + if (node.operator.type == TokenType.BANG) { + // x! is not a dynamic call, even if "x" is dynamic. + return; + } + _lintPrefixOrPostfixExpression(node, node.operand); + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + var property = node.identifier.name; + if (const { + 'hashCode', + 'noSuchMethod', + 'runtimeType', + 'toString', + }.contains(property)) { + // Special-cased; these exist on every object, even those typed "Object?". + return; + } + _lintIfDynamic(node.prefix); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + _lintPrefixOrPostfixExpression(node, node.operand); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + _lintIfDynamic(node.realTarget); + } + + bool _lintIfDynamic(Expression? node) { + if (node?.staticType is DynamicType) { + rule.reportLint(node); + return true; + } else { + return false; + } + } + + void _lintIfDynamicOrFunction(Expression node, {DartType? staticType}) { + staticType ??= node.staticType; + if (staticType == null) { + return; + } + if (staticType is DynamicType) { + rule.reportLint(node); + } + if (staticType.isDartCoreFunction) { + rule.reportLint(node); + } + } + + void _lintPrefixOrPostfixExpression(Expression root, Expression operand) { + if (_lintIfDynamic(operand)) { + return; + } + if (root is CompoundAssignmentExpression) { + if (root.readType is DynamicType) { + // An assignment expression can only be a dynamic call if it is a + // "compound assignment" (i.e. such as `x += 1`); so if `readType` is + // dynamic we should lint. + rule.reportLint(root); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_empty_else.dart b/pkg/linter/lib/src/rules/avoid_empty_else.dart new file mode 100644 index 00000000000..16ea88ce205 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_empty_else.dart @@ -0,0 +1,62 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid empty else statements.'; + +const _details = r''' +**AVOID** empty else statements. + +**BAD:** +```dart +if (x > y) + print("1"); +else ; + print("2"); +``` + +'''; + +class AvoidEmptyElse extends LintRule { + static const LintCode code = LintCode('avoid_empty_else', + "Empty statements are not allowed in an 'else' clause.", + correctionMessage: + 'Try removing the empty statement or removing the else clause.'); + + @override + LintCode get lintCode => code; + + AvoidEmptyElse() + : super( + name: 'avoid_empty_else', + description: _desc, + details: _details, + group: Group.errors); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addIfStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitIfStatement(IfStatement node) { + var elseStatement = node.elseStatement; + if (elseStatement is EmptyStatement && + !elseStatement.semicolon.isSynthetic) { + rule.reportLint(elseStatement); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_equals_and_hash_code_on_mutable_classes.dart b/pkg/linter/lib/src/rules/avoid_equals_and_hash_code_on_mutable_classes.dart new file mode 100644 index 00000000000..ab6927db446 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_equals_and_hash_code_on_mutable_classes.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = + r'Avoid overloading operator == and hashCode on classes not marked `@immutable`.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#avoid-defining-custom-equality-for-mutable-classes): + +**AVOID** overloading operator == and hashCode on classes not marked `@immutable`. + +If a class is not immutable, overloading `operator ==` and `hashCode` can +lead to unpredictable and undesirable behavior when used in collections. + +**BAD:** +```dart +class B { + String key; + const B(this.key); + @override + operator ==(other) => other is B && other.key == key; + @override + int get hashCode => key.hashCode; +} +``` + +**GOOD:** +```dart +@immutable +class A { + final String key; + const A(this.key); + @override + operator ==(other) => other is A && other.key == key; + @override + int get hashCode => key.hashCode; +} +``` + +NOTE: The lint checks the use of the `@immutable` annotation, and will trigger +even if the class is otherwise not mutable. Thus: + +**BAD:** +```dart +class C { + final String key; + const C(this.key); + @override + operator ==(other) => other is C && other.key == key; + @override + int get hashCode => key.hashCode; +} +``` + +'''; + +/// The name of the top-level variable used to mark a immutable class. +String _immutableVarName = 'immutable'; + +/// The name of `meta` library, used to define analysis annotations. +String _metaLibName = 'meta'; + +bool _isImmutable(Element? element) => + element is PropertyAccessorElement && + element.name == _immutableVarName && + element.library.name == _metaLibName; + +class AvoidEqualsAndHashCodeOnMutableClasses extends LintRule { + static const LintCode code = LintCode( + 'avoid_equals_and_hash_code_on_mutable_classes', + "The method '{0}' should not be overridden in classes not annotated with '@immutable'.", + correctionMessage: + "Try removing the override or annotating the class with '@immutable'."); + + AvoidEqualsAndHashCodeOnMutableClasses() + : super( + name: 'avoid_equals_and_hash_code_on_mutable_classes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.name.type == TokenType.EQ_EQ || isHashCode(node)) { + var classElement = _getClassForMethod(node); + if (classElement != null && !_hasImmutableAnnotation(classElement)) { + rule.reportLintForToken(node.firstTokenAfterCommentAndMetadata, + arguments: [node.name.lexeme]); + } + } + } + + ClassElement? _getClassForMethod(MethodDeclaration node) => + // todo (pq): should this be ClassOrMixinDeclaration ? + node.thisOrAncestorOfType()?.declaredElement; + + bool _hasImmutableAnnotation(ClassElement clazz) { + var inheritedAndSelfElements = [ + ...clazz.allSupertypes.map((t) => t.element), + clazz, + ]; + var inheritedAndSelfAnnotations = inheritedAndSelfElements + .expand((c) => c.metadata) + .map((m) => m.element); + return inheritedAndSelfAnnotations.any(_isImmutable); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_escaping_inner_quotes.dart b/pkg/linter/lib/src/rules/avoid_escaping_inner_quotes.dart new file mode 100644 index 00000000000..07116f7fe4a --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_escaping_inner_quotes.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid escaping inner quotes by converting surrounding quotes.'; + +const _details = r''' +Avoid escaping inner quotes by converting surrounding quotes. + +**BAD:** +```dart +var s = 'It\'s not fun'; +``` + +**GOOD:** +```dart +var s = "It's not fun"; +``` + +'''; + +class AvoidEscapingInnerQuotes extends LintRule { + static const LintCode code = LintCode( + 'avoid_escaping_inner_quotes', "Unnecessary escape of '{0}'.", + correctionMessage: "Try changing the outer quotes to '{1}'."); + + AvoidEscapingInnerQuotes() + : super( + name: 'avoid_escaping_inner_quotes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSimpleStringLiteral(this, visitor); + registry.addStringInterpolation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (node.isRaw || node.isMultiline) return; + _check(node, node.value, node.isSingleQuoted); + } + + @override + void visitStringInterpolation(StringInterpolation node) { + if (node.isRaw || node.isMultiline) return; + + var text = StringBuffer(); + for (var element in node.elements) { + if (element is InterpolationString) { + text.write(element.value); + } + } + _check(node, text.toString(), node.isSingleQuoted); + } + + void _check(AstNode node, String text, bool isSingleQuoted) { + if (_isChangeable(text, isSingleQuoted: isSingleQuoted)) { + rule.reportLint(node, + arguments: [isSingleQuoted ? "'" : '"', isSingleQuoted ? '"' : "'"]); + } + } + + bool _isChangeable(String text, {required bool isSingleQuoted}) => + text.contains(isSingleQuoted ? "'" : '"') && + !text.contains(isSingleQuoted ? '"' : "'"); +} diff --git a/pkg/linter/lib/src/rules/avoid_field_initializers_in_const_classes.dart b/pkg/linter/lib/src/rules/avoid_field_initializers_in_const_classes.dart new file mode 100644 index 00000000000..9733322ff91 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_field_initializers_in_const_classes.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid field initializers in const classes.'; + +const _details = r''' +**AVOID** field initializers in const classes. + +Instead of `final x = const expr;`, you should write `get x => const expr;` and +not allocate a useless field. As of April 2018 this is true for the VM, but not +for code that will be compiled to JS. + +**BAD:** +```dart +class A { + final a = const []; + const A(); +} +``` + +**GOOD:** +```dart +class A { + get a => const []; + const A(); +} +``` + +'''; + +class AvoidFieldInitializersInConstClasses extends LintRule { + static const LintCode code = LintCode( + 'avoid_field_initializers_in_const_classes', + "Fields in 'const' classes should not have initializers.", + correctionMessage: + 'Try converting the field to a getter or initialize the field in the ' + 'constructors.'); + + AvoidFieldInitializersInConstClasses() + : super( + name: 'avoid_field_initializers_in_const_classes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + registry.addConstructorFieldInitializer(this, visitor); + } +} + +class HasParameterReferenceVisitor extends RecursiveAstVisitor { + Iterable parameters; + + bool useParameter = false; + + HasParameterReferenceVisitor(this.parameters); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (parameters.contains(node.staticElement)) { + useParameter = true; + } else { + super.visitSimpleIdentifier(node); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorFieldInitializer(ConstructorFieldInitializer node) { + var declaration = node.parent; + if (declaration is ConstructorDeclaration) { + if (declaration.constKeyword == null) return; + var classDecl = declaration.thisOrAncestorOfType(); + if (classDecl == null) return; + // no lint if several constructors + var constructorCount = + classDecl.members.whereType().length; + if (constructorCount > 1) return; + + var visitor = HasParameterReferenceVisitor( + declaration.parameters.parameterElements); + node.expression.accept(visitor); + if (!visitor.useParameter) { + rule.reportLint(node); + } + } + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isStatic) return; + if (!node.fields.isFinal) return; + // only const class + var parent = node.parent; + if (parent is ClassDeclaration) { + var declaredElement = parent.declaredElement; + if (declaredElement == null) { + return; + } + if (declaredElement.constructors.every((e) => !e.isConst)) { + return; + } + for (var variable in node.fields.variables) { + if (variable.initializer != null) { + rule.reportLint(variable); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_final_parameters.dart b/pkg/linter/lib/src/rules/avoid_final_parameters.dart new file mode 100644 index 00000000000..606ed4a39f0 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_final_parameters.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid final for parameter declarations.'; + +const _details = r''' +**AVOID** declaring parameters as final. + +Declaring parameters as final can lead to unnecessarily verbose code, especially +when using the "parameter_assignments" rule. + +**BAD:** +```dart +void goodParameter(final String label) { // LINT + print(label); +} +``` + +**GOOD:** +```dart +void badParameter(String label) { // OK + print(label); +} +``` + +**BAD:** +```dart +void goodExpression(final int value) => print(value); // LINT +``` + +**GOOD:** +```dart +void badExpression(int value) => print(value); // OK +``` + +**BAD:** +```dart +[1, 4, 6, 8].forEach((final value) => print(value + 2)); // LINT +``` + +**GOOD:** +```dart +[1, 4, 6, 8].forEach((value) => print(value + 2)); // OK +``` + +'''; + +class AvoidFinalParameters extends LintRule { + static const LintCode code = LintCode( + 'avoid_final_parameters', "Parameters should not be marked as 'final'.", + correctionMessage: "Try removing the keyword 'final'."); + + AvoidFinalParameters() + : super( + name: 'avoid_final_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['prefer_final_parameters']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addFunctionExpression(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) => + _reportApplicableParameters(node.parameters); + + @override + void visitFunctionExpression(FunctionExpression node) => + _reportApplicableParameters(node.parameters); + + @override + void visitMethodDeclaration(MethodDeclaration node) => + _reportApplicableParameters(node.parameters); + + /// Report the lint for parameters in the [parameters] list that are final. + void _reportApplicableParameters(FormalParameterList? parameters) { + if (parameters != null) { + for (var param in parameters.parameters) { + if (param.isFinal) { + rule.reportLint(param); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_function_literals_in_foreach_calls.dart b/pkg/linter/lib/src/rules/avoid_function_literals_in_foreach_calls.dart new file mode 100644 index 00000000000..1e9a7ff75d2 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_function_literals_in_foreach_calls.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid using `forEach` with a function literal.'; + +const _details = r''' +**AVOID** using `forEach` with a function literal. + +The `for` loop enables a developer to be clear and explicit as to their intent. +A return in the body of the `for` loop returns from the body of the function, +where as a return in the body of the `forEach` closure only returns a value +for that iteration of the `forEach`. The body of a `for` loop can contain +`await`s, while the closure body of a `forEach` cannot. + +**BAD:** +```dart +people.forEach((person) { + ... +}); +``` + +**GOOD:** +```dart +for (var person in people) { + ... +} +``` +'''; + +bool _hasMethodChaining(MethodInvocation node) { + var exp = node.target; + while (exp is PrefixedIdentifier || + exp is MethodInvocation || + exp is PropertyAccess) { + if (exp is PrefixedIdentifier) { + exp = exp.prefix; + } else if (exp is MethodInvocation) { + return true; + } else if (exp is PropertyAccess) { + exp = exp.target; + } + } + return false; +} + +bool _isInsideCascade(AstNode node) => + node.thisOrAncestorMatching((n) => n is Statement || n is CascadeExpression) + is CascadeExpression; + +bool _isIterable(DartType? type) => + type != null && type.implementsInterface('Iterable', 'dart.core'); + +class AvoidFunctionLiteralsInForeachCalls extends LintRule { + static const LintCode code = LintCode( + 'avoid_function_literals_in_foreach_calls', + "Function literals shouldn't be passed to 'forEach'.", + correctionMessage: "Try using a 'for' loop."); + + AvoidFunctionLiteralsInForeachCalls() + : super( + name: 'avoid_function_literals_in_foreach_calls', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + var target = node.target; + if (target != null && + node.methodName.token.value() == 'forEach' && + node.argumentList.arguments.isNotEmpty && + node.argumentList.arguments.first is FunctionExpression && + _isIterable(target.staticType) && + !node.containsNullAwareInvocationInChain() && + !_hasMethodChaining(node) && + !_isInsideCascade(node)) { + rule.reportLint(node.function); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_implementing_value_types.dart b/pkg/linter/lib/src/rules/avoid_implementing_value_types.dart new file mode 100644 index 00000000000..cdaf29d28db --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_implementing_value_types.dart @@ -0,0 +1,140 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't implement classes that override `==`."; + +const _details = r''' +**DON'T** implement classes that override `==`. + +The `==` operator is contractually required to be an equivalence relation; +that is, symmetrically for all objects `o1` and `o2`, `o1 == o2` and `o2 == o1` +must either both be true, or both be false. + +> _NOTE_: Dart does not have true _value types_, so instead we consider a class +> that implements `==` as a _proxy_ for identifying value types. + +When using `implements`, you do not inherit the method body of `==`, making it +nearly impossible to follow the contract of `==`. Classes that override `==` +typically are usable directly in tests _without_ creating mocks or fakes as +well. For example, for a given class `Size`: + +```dart +class Size { + final int inBytes; + const Size(this.inBytes); + + @override + bool operator ==(Object other) => other is Size && other.inBytes == inBytes; + + @override + int get hashCode => inBytes.hashCode; +} +``` + +**BAD:** +```dart +class CustomSize implements Size { + final int inBytes; + const CustomSize(this.inBytes); + + int get inKilobytes => inBytes ~/ 1000; +} +``` + +**BAD:** +```dart +import 'package:test/test.dart'; +import 'size.dart'; + +class FakeSize implements Size { + int inBytes = 0; +} + +void main() { + test('should not throw on a size >1Kb', () { + expect(() => someFunction(FakeSize()..inBytes = 1001), returnsNormally); + }); +} +``` + +**GOOD:** +```dart +class ExtendedSize extends Size { + ExtendedSize(int inBytes) : super(inBytes); + + int get inKilobytes => inBytes ~/ 1000; +} +``` + +**GOOD:**: +```dart +import 'package:test/test.dart'; +import 'size.dart'; + +void main() { + test('should not throw on a size >1Kb', () { + expect(() => someFunction(Size(1001)), returnsNormally); + }); +} +``` + +'''; + +class AvoidImplementingValueTypes extends LintRule { + static const LintCode code = LintCode('avoid_implementing_value_types', + "Classes that override '==' should not be implemented.", + correctionMessage: + "Try removing the class from the 'implements' clause."); + + AvoidImplementingValueTypes() + : super( + name: 'avoid_implementing_value_types', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + var implementsClause = node.implementsClause; + if (implementsClause == null) { + return; + } + for (var interface in implementsClause.interfaces) { + var interfaceType = interface.type; + if (interfaceType is InterfaceType && + _overridesEquals(interfaceType.element)) { + rule.reportLint(interface); + } + } + } + + static bool _overridesEquals(InterfaceElement element) { + var method = element.lookUpConcreteMethod('==', element.library); + var enclosing = method?.enclosingElement; + return enclosing is ClassElement && !enclosing.isDartCoreObject; + } +} diff --git a/pkg/linter/lib/src/rules/avoid_init_to_null.dart b/pkg/linter/lib/src/rules/avoid_init_to_null.dart new file mode 100644 index 00000000000..5e1e643ccd2 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_init_to_null.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r"Don't explicitly initialize variables to null."; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#dont-explicitly-initialize-variables-to-null): + +**DON'T** explicitly initialize variables to `null`. + +If a variable has a non-nullable type or is `final`, +Dart reports a compile error if you try to use it +before it has been definitely initialized. +If the variable is nullable and not `const` or `final`, +then it is implicitly initialized to `null` for you. +There's no concept of "uninitialized memory" in Dart +and no need to explicitly initialize a variable to `null` to be "safe". +Adding `= null` is redundant and unneeded. + +**BAD:** +```dart +Item? bestDeal(List cart) { + Item? bestItem = null; + + for (final item in cart) { + if (bestItem == null || item.price < bestItem.price) { + bestItem = item; + } + } + + return bestItem; +} +``` + +**GOOD:** +```dart +Item? bestDeal(List cart) { + Item? bestItem; + + for (final item in cart) { + if (bestItem == null || item.price < bestItem.price) { + bestItem = item; + } + } + + return bestItem; +} +``` + +'''; + +class AvoidInitToNull extends LintRule { + static const LintCode code = LintCode( + 'avoid_init_to_null', "Redundant initialization to 'null'.", + correctionMessage: 'Try removing the initializer.'); + + AvoidInitToNull() + : super( + name: 'avoid_init_to_null', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addVariableDeclaration(this, visitor); + registry.addDefaultFormalParameter(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + final bool nnbdEnabled; + _Visitor(this.rule, this.context) + : nnbdEnabled = context.isEnabled(Feature.non_nullable); + + bool isNullable(DartType type) => + !nnbdEnabled || context.typeSystem.isNullable(type); + + @override + void visitDefaultFormalParameter(DefaultFormalParameter node) { + var declaredElement = node.declaredElement; + if (declaredElement == null) return; + + if (declaredElement is SuperFormalParameterElement) { + var superConstructorParameter = declaredElement.superConstructorParameter; + if (superConstructorParameter is! ParameterElement) return; + var defaultValue = superConstructorParameter.defaultValueCode ?? 'null'; + if (defaultValue != 'null') return; + } + + if (node.defaultValue.isNullLiteral && isNullable(declaredElement.type)) { + rule.reportLint(node); + } + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && + !node.isConst && + !node.isFinal && + node.initializer.isNullLiteral && + isNullable(declaredElement.type)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_js_rounded_ints.dart b/pkg/linter/lib/src/rules/avoid_js_rounded_ints.dart new file mode 100644 index 00000000000..6627c9505ab --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_js_rounded_ints.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid JavaScript rounded ints.'; + +const _details = r''' +**AVOID** integer literals that cannot be represented exactly when compiled to +JavaScript. + +When a program is compiled to JavaScript `int` and `double` become JavaScript +Numbers. Too large integers (`value < Number.MIN_SAFE_INTEGER` or +`value > Number.MAX_SAFE_INTEGER`) may be rounded to the closest Number value. + +For instance `1000000000000000001` cannot be represented exactly as a JavaScript +Number, so `1000000000000000000` will be used instead. + +**BAD:** +```dart +int value = 9007199254740995; +``` + +**GOOD:** +```dart +BigInt value = BigInt.parse('9007199254740995'); +``` + +'''; + +class AvoidJsRoundedInts extends LintRule { + static const LintCode code = LintCode( + 'avoid_js_rounded_ints', + "Integer literal can't be represented exactly when compiled to " + 'JavaScript.', + correctionMessage: "Try using a 'BigInt' to represent the value."); + + AvoidJsRoundedInts() + : super( + name: 'avoid_js_rounded_ints', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addIntegerLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isRounded(int? value) => value?.toDouble().toInt() != value; + @override + void visitIntegerLiteral(IntegerLiteral node) { + if (isRounded(node.value)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_multiple_declarations_per_line.dart b/pkg/linter/lib/src/rules/avoid_multiple_declarations_per_line.dart new file mode 100644 index 00000000000..5cdf90e87a0 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_multiple_declarations_per_line.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't declare multiple variables on a single line."; + +const _details = r''' +**DON'T** declare multiple variables on a single line. + +**BAD:** +```dart +String? foo, bar, baz; +``` + +**GOOD:** +```dart +String? foo; +String? bar; +String? baz; +``` + +'''; + +class AvoidMultipleDeclarationsPerLine extends LintRule { + static const LintCode code = LintCode('avoid_multiple_declarations_per_line', + 'Multiple variables declared on a single line.', + correctionMessage: + 'Try splitting the variable declarations into multiple lines.'); + + AvoidMultipleDeclarationsPerLine() + : super( + name: 'avoid_multiple_declarations_per_line', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addVariableDeclarationList(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitVariableDeclarationList(VariableDeclarationList node) { + var parent = node.parent; + if (parent is ForPartsWithDeclarations && parent.variables == node) return; + + var variables = node.variables; + if (variables.length > 1) { + var secondVariable = variables[1]; + rule.reportLintForToken(secondVariable.name); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_null_checks_in_equality_operators.dart b/pkg/linter/lib/src/rules/avoid_null_checks_in_equality_operators.dart new file mode 100644 index 00000000000..fc04ab6feee --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_null_checks_in_equality_operators.dart @@ -0,0 +1,152 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r"Don't check for null in custom == operators."; + +const _details = r''' +**DON'T** check for null in custom == operators. + +As null is a special value, no instance of any class (other than `Null`) can be +equivalent to it. Thus, it is redundant to check whether the other instance is +null. + +**BAD:** +```dart +class Person { + final String? name; + + @override + operator ==(Object? other) => + other != null && other is Person && name == other.name; +} +``` + +**GOOD:** +```dart +class Person { + final String? name; + + @override + operator ==(Object? other) => other is Person && name == other.name; +} +``` + +'''; + +bool _isComparingEquality(TokenType tokenType) => + tokenType == TokenType.BANG_EQ || tokenType == TokenType.EQ_EQ; + +bool _isComparingParameterWithNull(BinaryExpression node, Element? parameter) => + _isComparingEquality(node.operator.type) && + ((node.leftOperand.isNullLiteral && + _isParameter(node.rightOperand, parameter)) || + (node.rightOperand.isNullLiteral && + _isParameter(node.leftOperand, parameter))); + +bool _isParameter(Expression expression, Element? parameter) => + expression.canonicalElement == parameter; + +bool _isParameterWithQuestionQuestion( + BinaryExpression node, Element? parameter) => + node.operator.type == TokenType.QUESTION_QUESTION && + _isParameter(node.leftOperand, parameter); + +class AvoidNullChecksInEqualityOperators extends LintRule { + static const LintCode code = LintCode( + 'avoid_null_checks_in_equality_operators', + "Unnecessary null comparison in implementation of '=='.", + correctionMessage: 'Try removing the comparison.'); + + AvoidNullChecksInEqualityOperators() + : super( + name: 'avoid_null_checks_in_equality_operators', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = + _Visitor(this, nnbdEnabled: context.isEnabled(Feature.non_nullable)); + registry.addMethodDeclaration(this, visitor); + } +} + +class _BodyVisitor extends RecursiveAstVisitor { + final Element? parameter; + final LintRule rule; + _BodyVisitor(this.parameter, this.rule); + + @override + visitBinaryExpression(BinaryExpression node) { + if (_isParameterWithQuestionQuestion(node, parameter) || + _isComparingParameterWithNull(node, parameter)) { + rule.reportLint(node); + } + super.visitBinaryExpression(node); + } + + @override + visitMethodInvocation(MethodInvocation node) { + if (node.operator?.type == TokenType.QUESTION_PERIOD && + node.target.canonicalElement == parameter) { + rule.reportLint(node); + } + super.visitMethodInvocation(node); + } + + @override + visitPropertyAccess(PropertyAccess node) { + if (node.operator.type == TokenType.QUESTION_PERIOD && + node.target.canonicalElement == parameter) { + rule.reportLint(node); + } + super.visitPropertyAccess(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final bool nnbdEnabled; + + _Visitor(this.rule, {required this.nnbdEnabled}); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + var parameters = node.parameters?.parameters; + if (parameters == null) { + return; + } + + if (node.name.type != TokenType.EQ_EQ || parameters.length != 1) { + return; + } + + var parameter = parameters.first.declaredElement?.canonicalElement; + + // Analyzer will produce UNNECESSARY_NULL_COMPARISON_FALSE|TRUE + // See: https://github.com/dart-lang/linter/issues/2864 + if (nnbdEnabled && + parameter is VariableElement && + parameter.type.nullabilitySuffix != NullabilitySuffix.question) { + return; + } + + node.body.accept(_BodyVisitor(parameter, rule)); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_positional_boolean_parameters.dart b/pkg/linter/lib/src/rules/avoid_positional_boolean_parameters.dart new file mode 100644 index 00000000000..31613ec634d --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_positional_boolean_parameters.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid positional boolean parameters.'; + +const _details = r''' +**AVOID** positional boolean parameters. + +Positional boolean parameters are a bad practice because they are very +ambiguous. Using named boolean parameters is much more readable because it +inherently describes what the boolean value represents. + +**BAD:** +```dart +Task(true); +Task(false); +ListBox(false, true, true); +Button(false); +``` + +**GOOD:** +```dart +Task.oneShot(); +Task.repeating(); +ListBox(scroll: true, showScrollbars: true); +Button(ButtonState.enabled); +``` + +'''; + +class AvoidPositionalBooleanParameters extends LintRule { + static const LintCode code = LintCode('avoid_positional_boolean_parameters', + "'bool' parameters should be named parameters.", + correctionMessage: 'Try converting the parameter to a named parameter.'); + + AvoidPositionalBooleanParameters() + : super( + name: 'avoid_positional_boolean_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addConstructorDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addGenericFunctionType(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + void checkParams(List? parameters) { + var parameterToLint = parameters?.firstWhereOrNull(_isBoolean); + if (parameterToLint != null) { + rule.reportLint(parameterToLint); + } + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && !declaredElement.isPrivate) { + checkParams(node.parameters.parameters); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && !declaredElement.isPrivate) { + checkParams(node.functionExpression.parameters?.parameters); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && + !node.isSetter && + !declaredElement.isPrivate && + !node.isOperator && + !node.hasInheritedMethod && + !_isOverridingMember(declaredElement)) { + checkParams(node.parameters?.parameters); + } + } + + @override + void visitGenericFunctionType(GenericFunctionType node) { + checkParams(node.parameters.parameters); + } + + bool _isOverridingMember(Element member) { + var classElement = member.thisOrAncestorOfType(); + if (classElement == null) return false; + + var name = member.name; + if (name == null) return false; + + var libraryUri = classElement.library.source.uri; + return context.inheritanceManager + .getInherited(classElement.thisType, Name(libraryUri, name)) != + null; + } + + static bool _isBoolean(FormalParameter node) { + var type = node.declaredElement?.type; + return !node.isNamed && type is InterfaceType && type.isDartCoreBool; + } +} diff --git a/pkg/linter/lib/src/rules/avoid_print.dart b/pkg/linter/lib/src/rules/avoid_print.dart new file mode 100644 index 00000000000..9d293761ab9 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_print.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Avoid `print` calls in production code.'; + +const _details = r''' +**DO** avoid `print` calls in production code. + +For production code, consider using a logging framework. +If you are using Flutter, you can use `debugPrint` +or surround `print` calls with a check for `kDebugMode` + +**BAD:** +```dart +void f(int x) { + print('debug: $x'); + ... +} +``` + + +**GOOD:** +```dart +void f(int x) { + debugPrint('debug: $x'); + ... +} +``` + + +**GOOD:** +```dart +void f(int x) { + log('log: $x'); + ... +} +``` + + +**GOOD:** +```dart +void f(int x) { + if (kDebugMode) { + print('debug: $x'); + } + ... +} +``` +'''; + +class AvoidPrint extends LintRule { + static const LintCode code = LintCode( + 'avoid_print', "Don't invoke 'print' in production code.", + correctionMessage: 'Try using a logging framework.'); + + AvoidPrint() + : super( + name: 'avoid_print', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.methodName.staticElement.isDartCorePrint && !_isDebugOnly(node)) { + rule.reportLint(node.methodName); + } + + node.argumentList.arguments.forEach(_validateArgument); + } + + bool _isDebugOnly(Expression expression) { + AstNode? node = expression; + while (node != null) { + var parent = node.parent; + if (parent is IfStatement && node == parent.thenStatement) { + var condition = parent.expression; + if (condition is SimpleIdentifier && + isKDebugMode(condition.staticElement)) { + return true; + } + } else if (parent is FunctionBody) { + return false; + } + node = parent; + } + return false; + } + + void _validateArgument(Expression expression) { + if (expression is SimpleIdentifier) { + var element = expression.staticElement; + if (element is FunctionElement && + element.name == 'print' && + element.library.isDartCore) { + rule.reportLint(expression); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_private_typedef_functions.dart b/pkg/linter/lib/src/rules/avoid_private_typedef_functions.dart new file mode 100644 index 00000000000..37abe45d526 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_private_typedef_functions.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid private typedef functions.'; + +const _details = r''' +**AVOID** private typedef functions used only once. Prefer inline function +syntax. + +**BAD:** +```dart +typedef void _F(); +m(_F f); +``` + +**GOOD:** +```dart +m(void Function() f); +``` + +'''; + +class AvoidPrivateTypedefFunctions extends LintRule { + static const LintCode code = LintCode('avoid_private_typedef_functions', + 'The typedef is unnecessary because it is only used in one place.', + correctionMessage: 'Try inlining the type or using it in other places.'); + + AvoidPrivateTypedefFunctions() + : super( + name: 'avoid_private_typedef_functions', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addFunctionTypeAlias(this, visitor); + registry.addGenericTypeAlias(this, visitor); + } +} + +class _CountVisitor extends RecursiveAstVisitor { + final String type; + int count = 0; + _CountVisitor(this.type); + + @override + void visitNamedType(NamedType node) { + if (node.name2.lexeme == type) count++; + super.visitNamedType(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + _countAndReport(node.name); + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + if (node.typeParameters != null) return; + if (node.type is NamedType) return; + if (node.type is RecordTypeAnnotation) return; + + _countAndReport(node.name); + } + + void _countAndReport(Token identifier) { + var name = identifier.lexeme; + if (!Identifier.isPrivateName(name)) return; + + var visitor = _CountVisitor(name); + for (var unit in context.allUnits) { + unit.unit.accept(visitor); + } + if (visitor.count <= 1) { + rule.reportLintForToken(identifier); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_redundant_argument_values.dart b/pkg/linter/lib/src/rules/avoid_redundant_argument_values.dart new file mode 100644 index 00000000000..e5c4a09f88d --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_redundant_argument_values.dart @@ -0,0 +1,187 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid redundant argument values.'; + +const _details = r''' +**DON'T** pass an argument that matches the corresponding parameter's default +value. + +**BAD:** +```dart +void f({bool valWithDefault = true, bool? val}) { + ... +} + +void main() { + f(valWithDefault: true); +} +``` + +**GOOD:** +```dart +void f({bool valWithDefault = true, bool? val}) { + ... +} + +void main() { + f(valWithDefault: false); + f(); +} +``` +'''; + +class AvoidRedundantArgumentValues extends LintRule { + static const LintCode code = LintCode( + 'avoid_redundant_argument_values', + 'The value of the argument is redundant because it matches the default ' + 'value.', + correctionMessage: 'Try removing the argument.'); + + AvoidRedundantArgumentValues() + : super( + name: 'avoid_redundant_argument_values', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addEnumConstantArguments(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + registry.addFunctionExpressionInvocation(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + void check(ArgumentList argumentList) { + var arguments = argumentList.arguments; + if (arguments.isEmpty) { + return; + } + + for (var i = arguments.length - 1; i >= 0; --i) { + var arg = arguments[i]; + var param = arg.staticParameterElement; + if (arg is NamedExpression) { + arg = arg.expression; + } + checkArgument(arg, param); + if (param != null && param.isOptionalPositional) { + // Redundant arguments may be necessary to specify, in order to specify + // a non-redundant argument for the last optional positional parameter. + break; + } + } + } + + void checkArgument(Expression arg, ParameterElement? param) { + if (param == null || + param.declaration.isRequired || + param.hasRequired || + !param.isOptional) { + return; + } + var value = param.computeConstantValue(); + // todo(pq): reenable and do ecosystem cleanup (https://github.com/dart-lang/linter/issues/4368) + // if (value == null && arg is NullLiteral) { + // rule.reportLint(arg); + // } else ... + if (value != null && value.hasKnownValue) { + var expressionValue = context.evaluateConstant(arg).value; + if ((expressionValue?.hasKnownValue ?? false) && + expressionValue == value) { + rule.reportLint(arg); + } + } + } + + @override + void visitEnumConstantArguments(EnumConstantArguments node) { + check(node.argumentList); + } + + @override + visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + check(node.argumentList); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var constructor = node.constructorName.staticElement; + if (constructor != null && !constructor.isFactory) { + check(node.argumentList); + return; + } + + var redirectedConstructor = constructor?.redirectedConstructor; + while (redirectedConstructor?.redirectedConstructor != null) { + redirectedConstructor = redirectedConstructor?.redirectedConstructor; + } + if (redirectedConstructor == null) { + check(node.argumentList); + return; + } + + var parameters = redirectedConstructor.parameters; + + // If the constructor being called is a redirecting factory constructor, an + // argument is redundant if it is equal to the default value of the + // corresponding parameter on the _redirected constructor_, not this + // constructor, which may be different. + + var arguments = node.argumentList.arguments; + if (arguments.isEmpty) { + return; + } + + for (var i = arguments.length - 1; i >= 0; --i) { + var arg = arguments[i]; + ParameterElement? param; + if (arg is NamedExpression) { + param = parameters.firstWhereOrNull( + (p) => p.isNamed && p.name == arg.name.label.name); + } else { + // Count which positional argument we're at. + var positionalCount = + arguments.take(i + 1).where((a) => a is! NamedExpression).length; + var positionalIndex = positionalCount - 1; + if (positionalIndex < parameters.length) { + if (parameters[positionalIndex].isPositional) { + param = parameters[positionalIndex]; + } + } + } + checkArgument(arg, param); + if (param != null && param.isOptionalPositional) { + // Redundant arguments may be necessary to specify, in order to specify + // a non-redundant argument for the last optional positional parameter. + break; + } + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + check(node.argumentList); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_relative_lib_imports.dart b/pkg/linter/lib/src/rules/avoid_relative_lib_imports.dart new file mode 100644 index 00000000000..e1e47881ce7 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_relative_lib_imports.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid relative imports for files in `lib/`.'; + +const _details = r''' +**DO** avoid relative imports for files in `lib/`. + +When mixing relative and absolute imports it's possible to create confusion +where the same member gets imported in two different ways. An easy way to avoid +that is to ensure you have no relative imports that include `lib/` in their +paths. + +You can also use 'always_use_package_imports' to disallow relative imports +between files within `lib/`. + +**BAD:** +```dart +import 'package:foo/bar.dart'; + +import '../lib/baz.dart'; + +... +``` + +**GOOD:** +```dart +import 'package:foo/bar.dart'; + +import 'baz.dart'; + +... +``` + +'''; + +class AvoidRelativeLibImports extends LintRule { + static const LintCode code = LintCode('avoid_relative_lib_imports', + "Can't use a relative path to import a library in 'lib'.", + correctionMessage: + "Try fixing the relative path or changing the import to a 'package:' " + 'import.'); + + @override + LintCode get lintCode => code; + + AvoidRelativeLibImports() + : super( + name: 'avoid_relative_lib_imports', + description: _desc, + details: _details, + group: Group.errors); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isRelativeLibImport(ImportDirective node) { + // Relative paths from within the `lib` folder are covered by the + // `always_use_package_imports` lint. + var uriContent = node.uri.stringValue; + if (uriContent != null) { + var uri = Uri.tryParse(uriContent); + if (uri != null && uri.scheme.isEmpty) { + return uri.path.contains('/lib/'); + } + } + return false; + } + + @override + void visitImportDirective(ImportDirective node) { + if (isRelativeLibImport(node)) { + rule.reportLint(node.uri); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart b/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart new file mode 100644 index 00000000000..059087fce49 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_renaming_method_parameters.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r"Don't rename parameters of overridden methods."; + +const _details = r''' +**DON'T** rename parameters of overridden methods. + +Methods that override another method, but do not have their own documentation +comment, will inherit the overridden method's comment when `dart doc` produces +documentation. If the inherited method contains the name of the parameter (in +square brackets), then `dart doc` cannot link it correctly. + +**BAD:** +```dart +abstract class A { + m(a); +} + +abstract class B extends A { + m(b); +} +``` + +**GOOD:** +```dart +abstract class A { + m(a); +} + +abstract class B extends A { + m(a); +} +``` + +'''; + +class AvoidRenamingMethodParameters extends LintRule { + static const LintCode parameterCode = LintCode( + 'avoid_renaming_method_parameters', + "The parameter name '{0}' doesn't match the name '{1}' in the overridden " + 'method.', + correctionMessage: "Try changing the name to '{1}'."); + + AvoidRenamingMethodParameters() + : super( + name: 'avoid_renaming_method_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => parameterCode; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!isInLibDir(context.currentUnit.unit, context.package)) { + return; + } + + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isStatic) return; + if (node.documentationComment != null) return; + + var parentNode = node.parent; + if (parentNode is! Declaration) { + return; + } + var parentElement = parentNode.declaredElement; + // Note: there are no override semantics with extension methods. + if (parentElement is! InterfaceElement) { + return; + } + + var classElement = parentElement; + + if (classElement.isPrivate) return; + + var parentMethod = classElement.lookUpInheritedMethod( + node.name.lexeme, classElement.library); + + if (parentMethod == null) return; + + var nodeParams = node.parameters; + if (nodeParams == null) { + return; + } + + var parameters = nodeParams.parameters.where((p) => !p.isNamed).toList(); + var parentParameters = + parentMethod.parameters.where((p) => !p.isNamed).toList(); + var count = math.min(parameters.length, parentParameters.length); + for (var i = 0; i < count; i++) { + if (parentParameters.length <= i) break; + var paramIdentifier = parameters[i].name; + if (paramIdentifier != null && + paramIdentifier.lexeme != parentParameters[i].name) { + rule.reportLintForToken(paramIdentifier, + arguments: [paramIdentifier.lexeme, parentParameters[i].name]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_return_types_on_setters.dart b/pkg/linter/lib/src/rules/avoid_return_types_on_setters.dart new file mode 100644 index 00000000000..50b17c29757 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_return_types_on_setters.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid return types on setters.'; + +const _details = r''' +**AVOID** return types on setters. + +As setters do not return a value, declaring the return type of one is redundant. + +**BAD:** +```dart +void set speed(int ms); +``` + +**GOOD:** +```dart +set speed(int ms); +``` + +'''; + +class AvoidReturnTypesOnSetters extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'avoid_return_types_on_setters', 'Unnecessary return type on a setter.', + correctionMessage: 'Try removing the return type.'); + + AvoidReturnTypesOnSetters() + : super( + name: 'avoid_return_types_on_setters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + if (node.isSetter) { + if (node.returnType != null) { + rule.reportLint(node.returnType); + } + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isSetter) { + if (node.returnType != null) { + rule.reportLint(node.returnType); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_returning_null.dart b/pkg/linter/lib/src/rules/avoid_returning_null.dart new file mode 100644 index 00000000000..de9f190c304 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_returning_null.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Avoid returning null from members whose return type is bool, double, int,' + r' or num.'; + +const _details = r''' +**AVOID** returning null from members whose return type is bool, double, int, +or num. + +Functions that return primitive types such as bool, double, int, and num are +generally expected to return non-nullable values. Thus, returning null where a +primitive type was expected can lead to runtime exceptions. + +**BAD:** +```dart +bool getBool() => null; +num getNum() => null; +int getInt() => null; +double getDouble() => null; +``` + +**GOOD:** +```dart +bool getBool() => false; +num getNum() => -1; +int getInt() => -1; +double getDouble() => -1.0; +``` + +'''; + +bool _isPrimitiveType(DartType type) => + type is InterfaceType && + (type.isDartCoreBool || + type.isDartCoreDouble || + type.isDartCoreInt || + type.isDartCoreNum); + +class AvoidReturningNull extends LintRule { + static const LintCode code = LintCode( + 'avoid_returning_null', + "Don't return 'null' when the return type is 'bool', 'double', 'int', " + "or 'num'.", + correctionMessage: "Try returning a sentinel value other than 'null'."); + + AvoidReturningNull() + : super( + name: 'avoid_returning_null', + description: _desc, + details: _details, + state: State.deprecated(since: dart2_12), + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + // This lint does not make sense in the context of nullability. + // Long-term it should be deprecated and slated for removal. + // See: https://github.com/dart-lang/linter/issues/2636 + if (!context.isEnabled(Feature.non_nullable)) { + var visitor = _Visitor(this); + registry.addFunctionExpression(this, visitor); + registry.addMethodDeclaration(this, visitor); + } + } +} + +class _BodyVisitor extends RecursiveAstVisitor { + final LintRule rule; + _BodyVisitor(this.rule); + + @override + visitFunctionExpression(FunctionExpression node) { + // Skip Function expressions. + } + + @override + visitReturnStatement(ReturnStatement node) { + if (node.expression.isNullLiteral) { + rule.reportLint(node); + } + + super.visitReturnStatement(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionExpression(FunctionExpression node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && + _isPrimitiveType(declaredElement.returnType)) { + _visitFunctionBody(node.body); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement != null && + _isPrimitiveType(declaredElement.returnType)) { + _visitFunctionBody(node.body); + } + } + + void _visitFunctionBody(FunctionBody node) { + if (node is ExpressionFunctionBody && node.expression.isNullLiteral) { + rule.reportLint(node); + return; + } + + node.accept(_BodyVisitor(rule)); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_returning_null_for_future.dart b/pkg/linter/lib/src/rules/avoid_returning_null_for_future.dart new file mode 100644 index 00000000000..bac277d3d8d --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_returning_null_for_future.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid returning null for Future.'; + +const _details = r''' +**AVOID** returning null for Future. + +It is almost always wrong to return `null` for a `Future`. Most of the time the +developer simply forgot to put an `async` keyword on the function. + +'''; + +class AvoidReturningNullForFuture extends LintRule { + static const LintCode code = LintCode('avoid_returning_null_for_future', + "Don't return 'null' when the return type is 'Future'.", + correctionMessage: + "Try making the function 'async', or returning 'Future.value(null)'."); + + AvoidReturningNullForFuture() + : super( + name: 'avoid_returning_null_for_future', + description: _desc, + details: _details, + state: State.deprecated(since: dart2_12), + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + // In a Null Safety library, this lint is covered by other formal static + // analysis. + if (!context.isEnabled(Feature.non_nullable)) { + var visitor = _Visitor(this); + registry.addExpressionFunctionBody(this, visitor); + registry.addReturnStatement(this, visitor); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _visit(node, node.expression); + } + + @override + void visitReturnStatement(ReturnStatement node) { + var expression = node.expression; + if (expression != null) { + _visit(node, expression); + } + } + + void _visit(AstNode node, Expression expression) { + if (expression.staticType?.isDartCoreNull != true) { + return; + } + + var parent = node.thisOrAncestorMatching( + (e) => e is FunctionExpression || e is MethodDeclaration); + if (parent == null) return; + + DartType? returnType; + bool isAsync; + if (parent is FunctionExpression) { + returnType = parent.declaredElement?.returnType; + isAsync = parent.body.isAsynchronous; + } else if (parent is MethodDeclaration) { + returnType = parent.declaredElement?.returnType; + isAsync = parent.body.isAsynchronous; + } else { + throw StateError('unexpected type'); + } + if (returnType != null && returnType.isDartAsyncFuture && !isAsync) { + rule.reportLint(expression); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_returning_null_for_void.dart b/pkg/linter/lib/src/rules/avoid_returning_null_for_void.dart new file mode 100644 index 00000000000..9fbfe2190b4 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_returning_null_for_void.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid returning null for void.'; + +const _details = r''' +**AVOID** returning null for void. + +In a large variety of languages `void` as return type is used to indicate that +a function doesn't return anything. Dart allows returning `null` in functions +with `void` return type but it also allow using `return;` without specifying any +value. To have a consistent way you should not return `null` and only use an +empty return. + +**BAD:** +```dart +void f1() { + return null; +} +Future f2() async { + return null; +} +``` + +**GOOD:** +```dart +void f1() { + return; +} +Future f2() async { + return; +} +``` + +'''; + +class AvoidReturningNullForVoid extends LintRule { + static const LintCode fromFunction = LintCode('avoid_returning_null_for_void', + "Don't return 'null' from a function with a return type of 'void'.", + correctionMessage: "Try removing the 'null'."); + + static const LintCode fromMethod = LintCode('avoid_returning_null_for_void', + "Don't return 'null' from a method with a return type of 'void'.", + correctionMessage: "Try removing the 'null'."); + + AvoidReturningNullForVoid() + : super( + name: 'avoid_returning_null_for_void', + description: _desc, + details: _details, + group: Group.style); + + @override + List get lintCodes => [fromFunction, fromMethod]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addExpressionFunctionBody(this, visitor); + registry.addReturnStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _visit(node, node.expression); + } + + @override + void visitReturnStatement(ReturnStatement node) { + if (node.expression != null) { + _visit(node, node.expression); + } + } + + void _visit(AstNode node, Expression? expression) { + if (expression is! NullLiteral) { + return; + } + + var parent = node.thisOrAncestorMatching( + (e) => e is FunctionExpression || e is MethodDeclaration); + if (parent == null) return; + + DartType? type; + bool? isAsync; + LintCode code; + if (parent is FunctionExpression) { + type = parent.declaredElement?.returnType; + isAsync = parent.body.isAsynchronous; + code = AvoidReturningNullForVoid.fromFunction; + } else if (parent is MethodDeclaration) { + type = parent.declaredElement?.returnType; + isAsync = parent.body.isAsynchronous; + code = AvoidReturningNullForVoid.fromMethod; + } else { + throw StateError('unexpected type'); + } + if (type == null) return; + + if (!isAsync && type is VoidType) { + rule.reportLint(node, errorCode: code); + } else if (isAsync && + type.isDartAsyncFuture && + (type as InterfaceType).typeArguments.first is VoidType) { + rule.reportLint(node, errorCode: code); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_returning_this.dart b/pkg/linter/lib/src/rules/avoid_returning_this.dart new file mode 100644 index 00000000000..7f392e2836a --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_returning_this.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Avoid returning this from methods just to enable a fluent interface.'; + +const _details = r''' +**AVOID** returning this from methods just to enable a fluent interface. + +Returning `this` from a method is redundant; Dart has a cascade operator which +allows method chaining universally. + +Returning `this` is allowed for: + +- operators +- methods with a return type different of the current class +- methods defined in parent classes / mixins or interfaces +- methods defined in extensions + +**BAD:** +```dart +var buffer = StringBuffer() + .write('one') + .write('two') + .write('three'); +``` + +**GOOD:** +```dart +var buffer = StringBuffer() + ..write('one') + ..write('two') + ..write('three'); +``` + +'''; + +bool _returnsThis(ReturnStatement node) => node.expression is ThisExpression; + +class AvoidReturningThis extends LintRule { + static const LintCode code = LintCode( + 'avoid_returning_this', "Don't return 'this' from a method.", + correctionMessage: + "Try changing the return type to 'void' and removing the return."); + + AvoidReturningThis() + : super( + name: 'avoid_returning_this', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _BodyVisitor extends RecursiveAstVisitor { + List returnStatements = []; + + bool foundNonThisReturn = false; + + List collectReturns(BlockFunctionBody body) { + body.accept(this); + return returnStatements; + } + + @override + visitFunctionExpression(FunctionExpression node) { + // Short-circuit visiting on Function expressions. + } + + @override + visitReturnStatement(ReturnStatement node) { + // Short-circuit if we've encountered a non-this return. + if (foundNonThisReturn) return; + // Short-circuit if not returning this. + if (!_returnsThis(node)) { + foundNonThisReturn = true; + returnStatements.clear(); + return; + } + returnStatements.add(node); + super.visitReturnStatement(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isOperator) return; + + var parent = node.parent; + if (parent is ClassDeclaration || + parent is EnumDeclaration || + parent is MixinDeclaration) { + if (node.isOverride) { + return; + } + + var returnType = node.declaredElement?.returnType; + if (returnType is InterfaceType && + // ignore: cast_nullable_to_non_nullable + returnType.element == (parent as Declaration).declaredElement) { + } else { + return; + } + } else { + // Ignore Extensions. + return; + } + + var body = node.body; + if (body is BlockFunctionBody) { + var returnStatements = _BodyVisitor().collectReturns(body); + if (returnStatements.isNotEmpty) { + rule.reportLint(returnStatements.first.expression); + } + } else if (body is ExpressionFunctionBody) { + if (body.expression is ThisExpression) { + rule.reportLintForToken(node.name); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_setters_without_getters.dart b/pkg/linter/lib/src/rules/avoid_setters_without_getters.dart new file mode 100644 index 00000000000..cc1360942a5 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_setters_without_getters.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid setters without getters.'; + +const _details = r''' +**DON'T** define a setter without a corresponding getter. + +Defining a setter without defining a corresponding getter can lead to logical +inconsistencies. Doing this could allow you to set a property to some value, +but then upon observing the property's value, it could easily be different. + +**BAD:** +```dart +class Bad { + int l, r; + + set length(int newLength) { + r = l + newLength; + } +} +``` + +**GOOD:** +```dart +class Good { + int l, r; + + int get length => r - l; + + set length(int newLength) { + r = l + newLength; + } +} +``` + +'''; + +class AvoidSettersWithoutGetters extends LintRule { + static const LintCode code = LintCode( + 'avoid_setters_without_getters', 'Setter has no corresponding getter.', + correctionMessage: + 'Try adding a corresponding getter or removing the setter.'); + + AvoidSettersWithoutGetters() + : super( + name: 'avoid_setters_without_getters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + // todo(pq): consider visiting mixin declarations + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + visitMembers(node.members); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + visitMembers(node.members); + } + + void visitMembers(NodeList members) { + for (var member in members.whereType()) { + if (member.isSetter && + member.lookUpInheritedConcreteSetter() == null && + member.lookUpGetter() == null) { + rule.reportLintForToken(member.name); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_shadowing_type_parameters.dart b/pkg/linter/lib/src/rules/avoid_shadowing_type_parameters.dart new file mode 100644 index 00000000000..f9f7789a00c --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_shadowing_type_parameters.dart @@ -0,0 +1,134 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid shadowing type parameters.'; + +const _details = r''' +**AVOID** shadowing type parameters. + +**BAD:** +```dart +class A { + void fn() {} +} +``` + +**GOOD:** +```dart +class A { + void fn() {} +} +``` + +'''; + +class AvoidShadowingTypeParameters extends LintRule { + static const LintCode code = LintCode('avoid_shadowing_type_parameters', + "The type parameter '{0}' shadows a type parameter from the enclosing {1}.", + correctionMessage: 'Try renaming one of the type parameters.'); + + AvoidShadowingTypeParameters() + : super( + name: 'avoid_shadowing_type_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclarationStatement(this, visitor); + registry.addGenericTypeAlias(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { + var functionExpression = node.functionDeclaration.functionExpression; + if (functionExpression.typeParameters != null) { + _checkAncestorParameters(functionExpression.typeParameters, node); + } + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + var typeParameters = node.functionType?.typeParameters; + if (typeParameters != null) { + _checkForShadowing(typeParameters, node.typeParameters, 'typedef'); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.typeParameters == null) { + return; + } + + // Static methods have nothing above them to shadow. + if (!node.isStatic) { + _checkAncestorParameters(node.typeParameters, node); + } + } + + // Check the ancestors of [node] for type parameter shadowing. + void _checkAncestorParameters( + TypeParameterList? typeParameters, AstNode node) { + var parent = node.parent; + + while (parent != null) { + if (parent is ClassDeclaration) { + _checkForShadowing(typeParameters, parent.typeParameters, 'class'); + } else if (parent is EnumDeclaration) { + _checkForShadowing(typeParameters, parent.typeParameters, 'enum'); + } else if (parent is ExtensionDeclaration) { + _checkForShadowing(typeParameters, parent.typeParameters, 'extension'); + } else if (parent is ExtensionTypeDeclaration) { + _checkForShadowing( + typeParameters, parent.typeParameters, 'extension type'); + } else if (parent is MethodDeclaration) { + _checkForShadowing(typeParameters, parent.typeParameters, 'method'); + } else if (parent is MixinDeclaration) { + _checkForShadowing(typeParameters, parent.typeParameters, 'mixin'); + } else if (parent is FunctionDeclaration) { + _checkForShadowing(typeParameters, + parent.functionExpression.typeParameters, 'function'); + } + parent = parent.parent; + } + } + + // Check whether any of [typeParameters] shadow [ancestorTypeParameters]. + void _checkForShadowing(TypeParameterList? typeParameters, + TypeParameterList? ancestorTypeParameters, String ancestorKind) { + if (typeParameters == null || ancestorTypeParameters == null) { + return; + } + + var ancestorTypeParameterNames = ancestorTypeParameters.typeParameters + .map((tp) => tp.name.lexeme) + .toSet(); + + for (var parameter in typeParameters.typeParameters) { + if (ancestorTypeParameterNames.contains(parameter.name.lexeme)) { + rule.reportLint(parameter, + arguments: [parameter.name.lexeme, ancestorKind]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_single_cascade_in_expression_statements.dart b/pkg/linter/lib/src/rules/avoid_single_cascade_in_expression_statements.dart new file mode 100644 index 00000000000..7339e9ba242 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_single_cascade_in_expression_statements.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid single cascade in expression statements.'; + +const _details = r''' +**AVOID** single cascade in expression statements. + +**BAD:** +```dart +o..m(); +``` + +**GOOD:** +```dart +o.m(); +``` + +'''; + +class AvoidSingleCascadeInExpressionStatements extends LintRule { + static const LintCode code = LintCode( + 'avoid_single_cascade_in_expression_statements', + 'Unnecessary cascade expression.', + correctionMessage: "Try using the operator '{0}'."); + + AvoidSingleCascadeInExpressionStatements() + : super( + name: 'avoid_single_cascade_in_expression_statements', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCascadeExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + String operatorFor(Expression section) { + Token? operator; + if (section is PropertyAccess) { + operator = section.operator; + } else if (section is MethodInvocation) { + operator = section.operator; + } + if (operator?.type == TokenType.PERIOD_PERIOD_PERIOD_QUESTION) { + return '?.'; + } + return '.'; + } + + @override + void visitCascadeExpression(CascadeExpression node) { + var sections = node.cascadeSections; + if (sections.length == 1 && node.parent is ExpressionStatement) { + var operator = operatorFor(sections[0]); + rule.reportLint(node, arguments: [operator]); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_slow_async_io.dart b/pkg/linter/lib/src/rules/avoid_slow_async_io.dart new file mode 100644 index 00000000000..5b3d746a3e2 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_slow_async_io.dart @@ -0,0 +1,139 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid slow async `dart:io` methods.'; + +const _details = r''' +**AVOID** using the following asynchronous file I/O methods because they are +much slower than their synchronous counterparts. + +* `Directory.exists` +* `Directory.stat` +* `File.lastModified` +* `File.exists` +* `File.stat` +* `FileSystemEntity.isDirectory` +* `FileSystemEntity.isFile` +* `FileSystemEntity.isLink` +* `FileSystemEntity.type` + +**BAD:** +```dart +import 'dart:io'; + +Future someFunction() async { + var file = File('/path/to/my/file'); + var now = DateTime.now(); + if ((await file.lastModified()).isBefore(now)) print('before'); // LINT +} +``` + +**GOOD:** +```dart +import 'dart:io'; + +Future someFunction() async { + var file = File('/path/to/my/file'); + var now = DateTime.now(); + if (file.lastModifiedSync().isBefore(now)) print('before'); // OK +} +``` + +'''; + +const List _dirMethodNames = [ + 'exists', + 'stat', +]; + +const List _fileMethodNames = [ + 'lastModified', + 'exists', + 'stat', +]; + +const List _fileSystemEntityMethodNames = [ + 'isDirectory', + 'isFile', + 'isLink', + 'type', +]; + +class AvoidSlowAsyncIo extends LintRule { + static const LintCode code = LintCode( + 'avoid_slow_async_io', "Use of an async 'dart:io' method.", + correctionMessage: 'Try using the synchronous version of the method.'); + + AvoidSlowAsyncIo() + : super( + name: 'avoid_slow_async_io', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.argumentList.arguments.isEmpty) { + var type = node.target?.staticType; + _checkFileMethods(node, type); + _checkDirectoryMethods(node, type); + return; + } else { + _checkFileSystemEntityMethods(node); + return; + } + } + + void _checkDirectoryMethods(MethodInvocation node, DartType? type) { + if (type.extendsClass('Directory', 'dart.io')) { + if (_dirMethodNames.contains(node.methodName.name)) { + rule.reportLint(node); + } + } + } + + void _checkFileMethods(MethodInvocation node, DartType? type) { + if (type.extendsClass('File', 'dart.io')) { + if (_fileMethodNames.contains(node.methodName.name)) { + rule.reportLint(node); + } + } + } + + void _checkFileSystemEntityMethods(MethodInvocation node) { + var target = node.target; + if (target is Identifier) { + var elem = target.staticElement; + if (elem is ClassElement && elem.name == 'FileSystemEntity') { + if (_fileSystemEntityMethodNames.contains(node.methodName.name)) { + rule.reportLint(node); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_type_to_string.dart b/pkg/linter/lib/src/rules/avoid_type_to_string.dart new file mode 100644 index 00000000000..e78b4310dd9 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_type_to_string.dart @@ -0,0 +1,162 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Avoid .toString() in production code since results may be minified.'; + +const _details = r''' +**DO** avoid calls to .toString() in production code, since it does not +contractually return the user-defined name of the Type (or underlying class). +Development-mode compilers where code size is not a concern use the full name, +but release-mode compilers often choose to minify these symbols. + +**BAD:** +```dart +void bar(Object other) { + if (other.runtimeType.toString() == 'Bar') { + doThing(); + } +} + +Object baz(Thing myThing) { + return getThingFromDatabase(key: myThing.runtimeType.toString()); +} +``` + +**GOOD:** +```dart +void bar(Object other) { + if (other is Bar) { + doThing(); + } +} + +class Thing { + String get thingTypeKey => ... +} + +Object baz(Thing myThing) { + return getThingFromDatabase(key: myThing.thingTypeKey); +} +``` + +'''; + +class AvoidTypeToString extends LintRule { + static const LintCode code = LintCode('avoid_type_to_string', + "Using 'toString' on a 'Type' is not safe in production code."); + + AvoidTypeToString() + : super( + name: 'avoid_type_to_string', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = + _Visitor(this, context.typeSystem, context.typeProvider.typeType); + // Gathering meta information at these nodes. + // Nodes visited in DFS, so this will be called before + // each visitMethodInvocation. + registry.addClassDeclaration(this, visitor); + registry.addMixinDeclaration(this, visitor); + registry.addExtensionDeclaration(this, visitor); + + registry.addArgumentList(this, visitor); + + // Actually checking things at these nodes. + // Also delegates to visitArgumentList. + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final TypeSystem typeSystem; + final InterfaceType typeType; + + // Null if there is no logical `this` in the given context. + InterfaceType? thisType; + + _Visitor(this.rule, this.typeSystem, this.typeType); + + @override + void visitArgumentList(ArgumentList node) { + node.arguments.forEach(_validateArgument); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + thisType = node.declaredElement?.thisType; + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + var extendedType = node.declaredElement?.extendedType; + // Might not be InterfaceType. Ex: visiting an extension on a dynamic type. + thisType = extendedType is InterfaceType ? extendedType : null; + } + + @override + void visitMethodInvocation(MethodInvocation node) { + visitArgumentList(node.argumentList); + + var staticType = node.realTarget?.staticType; + var targetType = staticType is InterfaceType ? staticType : thisType; + _reportIfToStringOnCoreTypeClass(targetType, node.methodName); + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + thisType = node.declaredElement?.thisType; + } + + bool _isSimpleIdDeclByCoreObj(SimpleIdentifier simpleIdentifier) { + var encloser = simpleIdentifier.staticElement?.enclosingElement; + return encloser is ClassElement && encloser.isDartCoreObject; + } + + bool _isToStringOnCoreTypeClass( + InterfaceType? targetType, SimpleIdentifier methodIdentifier) => + targetType != null && + methodIdentifier.name == 'toString' && + _isSimpleIdDeclByCoreObj(methodIdentifier) && + typeSystem.isSubtypeOf(targetType, typeType); + + void _reportIfToStringOnCoreTypeClass( + InterfaceType? targetType, SimpleIdentifier methodIdentifier) { + if (_isToStringOnCoreTypeClass(targetType, methodIdentifier)) { + rule.reportLint(methodIdentifier); + } + } + + void _validateArgument(Expression expression) { + if (expression is PropertyAccess) { + var expressionType = expression.realTarget.staticType; + var targetType = + (expressionType is InterfaceType) ? expressionType : thisType; + _reportIfToStringOnCoreTypeClass(targetType, expression.propertyName); + } else if (expression is PrefixedIdentifier) { + var prefixType = expression.prefix.staticType; + if (prefixType is InterfaceType) { + _reportIfToStringOnCoreTypeClass(prefixType, expression.identifier); + } + } else if (expression is SimpleIdentifier) { + _reportIfToStringOnCoreTypeClass(thisType, expression); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_types_as_parameter_names.dart b/pkg/linter/lib/src/rules/avoid_types_as_parameter_names.dart new file mode 100644 index 00000000000..1401a6c5093 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_types_as_parameter_names.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid types as parameter names.'; + +const _details = r''' +**AVOID** using a parameter name that is the same as an existing type. + +**BAD:** +```dart +m(f(int)); +``` + +**GOOD:** +```dart +m(f(int v)); +``` + +'''; + +class AvoidTypesAsParameterNames extends LintRule { + static const LintCode code = LintCode('avoid_types_as_parameter_names', + "The parameter name '{0}' matches a visible type name.", + correctionMessage: + 'Try adding a name for the parameter or changing the parameter name ' + 'to not match an existing type.'); + + AvoidTypesAsParameterNames() + : super( + name: 'avoid_types_as_parameter_names', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addFormalParameterList(this, visitor); + registry.addCatchClause(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCatchClause(CatchClause node) { + var parameter = node.exceptionParameter; + if (parameter != null && _isTypeName(node, parameter.name)) { + rule.reportLint(parameter, arguments: [parameter.name.lexeme]); + } + } + + @override + void visitFormalParameterList(FormalParameterList node) { + for (var parameter in node.parameters) { + var declaredElement = parameter.declaredElement; + var name = parameter.name; + if (declaredElement != null && + declaredElement is! FieldFormalParameterElement && + declaredElement.hasImplicitType && + name != null && + _isTypeName(node, name)) { + rule.reportLintForToken(name, arguments: [name.lexeme]); + } + } + } + + bool _isTypeName(AstNode scope, Token name) { + var result = context.resolveNameInScope(name.lexeme, false, scope); + if (result.isRequestedName) { + var element = result.element; + return element is ClassElement || + element is TypeAliasElement || + element is TypeParameterElement; + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/avoid_types_on_closure_parameters.dart b/pkg/linter/lib/src/rules/avoid_types_on_closure_parameters.dart new file mode 100644 index 00000000000..105e4c76162 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_types_on_closure_parameters.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid annotating types for function expression parameters.'; + +const _details = r''' +**AVOID** annotating types for function expression parameters. + +Annotating types for function expression parameters is usually unnecessary +because the parameter types can almost always be inferred from the context, +thus making the practice redundant. + +**BAD:** +```dart +var names = people.map((Person person) => person.name); +``` + +**GOOD:** +```dart +var names = people.map((person) => person.name); +``` + +'''; + +class AvoidTypesOnClosureParameters extends LintRule { + static const LintCode code = LintCode('avoid_types_on_closure_parameters', + 'Unnecessary type annotation on a function expression parameter.', + correctionMessage: 'Try removing the type annotation.'); + + AvoidTypesOnClosureParameters() + : super( + name: 'avoid_types_on_closure_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['always_specify_types']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionExpression(this, visitor); + } +} + +class AvoidTypesOnClosureParametersVisitor extends SimpleAstVisitor { + LintRule rule; + + AvoidTypesOnClosureParametersVisitor(this.rule); + + @override + void visitDefaultFormalParameter(DefaultFormalParameter node) { + node.parameter.accept(this); + } + + @override + void visitFunctionExpression(FunctionExpression node) { + if (node.parent is FunctionDeclaration) { + return; + } + var parameterList = node.parameters?.parameters; + if (parameterList != null) { + for (var parameter in parameterList) { + parameter.accept(this); + } + } + } + + @override + void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { + rule.reportLint(node); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter node) { + var type = node.type; + if (type is NamedType && type.type is! DynamicType) { + rule.reportLint(node.type); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionExpression(FunctionExpression node) { + var visitor = AvoidTypesOnClosureParametersVisitor(rule); + visitor.visitFunctionExpression(node); + } +} diff --git a/pkg/linter/lib/src/rules/avoid_unnecessary_containers.dart b/pkg/linter/lib/src/rules/avoid_unnecessary_containers.dart new file mode 100644 index 00000000000..1075710d9f8 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_unnecessary_containers.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Avoid unnecessary containers.'; + +const _details = r''' +**AVOID** wrapping widgets in unnecessary containers. + +Wrapping a widget in `Container` with no other parameters set has no effect +and makes code needlessly more complex. + +**BAD:** +```dart +Widget buildRow() { + return Container( + child: Row( + children: [ + const MyLogo(), + const Expanded( + child: Text('...'), + ), + ], + ) + ); +} +``` + +**GOOD:** +```dart +Widget buildRow() { + return Row( + children: [ + const MyLogo(), + const Expanded( + child: Text('...'), + ), + ], + ); +} +``` +'''; + +class AvoidUnnecessaryContainers extends LintRule { + static const LintCode code = LintCode( + 'avoid_unnecessary_containers', "Unnecessary instance of 'Container'.", + correctionMessage: + "Try removing the 'Container' (but not it's children) from the " + 'widget tree.'); + + AvoidUnnecessaryContainers() + : super( + name: 'avoid_unnecessary_containers', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isWidgetType(node.staticType)) { + return; + } + var parent = node.parent; + if (parent is NamedExpression && parent.name.label.name == 'child') { + var args = parent.thisOrAncestorOfType(); + if (args?.arguments.length == 1) { + var parentCreation = + parent.thisOrAncestorOfType(); + if (parentCreation != null) { + if (isExactWidgetTypeContainer(parentCreation.staticType)) { + rule.reportLint(parentCreation.constructorName); + } + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_unused_constructor_parameters.dart b/pkg/linter/lib/src/rules/avoid_unused_constructor_parameters.dart new file mode 100644 index 00000000000..99bed078d10 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_unused_constructor_parameters.dart @@ -0,0 +1,98 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Avoid defining unused parameters in constructors.'; + +const _details = r''' +**AVOID** defining unused parameters in constructors. + +**BAD:** +```dart +class BadOne { + BadOne(int unusedParameter, [String unusedPositional]); +} + +class BadTwo { + int c; + + BadTwo(int a, int b, int x) { + c = a + b; + } +} +``` + +'''; + +class AvoidUnusedConstructorParameters extends LintRule { + static const LintCode code = LintCode('avoid_unused_constructor_parameters', + "The parameter '{0}' is not used in the constructor.", + correctionMessage: 'Try using the parameter or removing it.'); + + AvoidUnusedConstructorParameters() + : super( + name: 'avoid_unused_constructor_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _ConstructorVisitor extends RecursiveAstVisitor { + final ConstructorDeclaration element; + final Set unusedParameters; + + _ConstructorVisitor(this.element) + : unusedParameters = element.parameters.parameters.where((p) { + var element = p.declaredElement; + return element != null && + element is! FieldFormalParameterElement && + element is! SuperFormalParameterElement && + !element.hasDeprecated && + !element.name.isJustUnderscores; + }).toSet(); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + unusedParameters + .removeWhere((p) => node.staticElement == p.declaredElement); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + if (node.redirectedConstructor != null) return; + if (node.externalKeyword != null) return; + + var constructorVisitor = _ConstructorVisitor(node); + node.body.visitChildren(constructorVisitor); + for (var i in node.initializers) { + i.visitChildren(constructorVisitor); + } + + for (var parameter in constructorVisitor.unusedParameters) { + rule.reportLint(parameter, arguments: [parameter.name!.lexeme]); + } + } +} diff --git a/pkg/linter/lib/src/rules/avoid_void_async.dart b/pkg/linter/lib/src/rules/avoid_void_async.dart new file mode 100644 index 00000000000..264b2b3868a --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_void_async.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid async functions that return void.'; + +const _details = r''' +**DO** mark async functions as returning Future. + +When declaring an async method or function which does not return a value, +declare that it returns `Future` and not just `void`. + +**BAD:** +```dart +void f() async {} +void f2() async => null; +``` + +**GOOD:** +```dart +Future f() async {} +Future f2() async => null; +``` + +**EXCEPTION:** + +An exception is made for top-level `main` functions, where the `Future` +annotation *can* (and generally should) be dropped in favor of `void`. + +**GOOD:** +```dart +Future f() async {} + +void main() async { + await f(); +} +``` +'''; + +class AvoidVoidAsync extends LintRule { + static const LintCode code = LintCode('avoid_void_async', + "The return type should be '{0}' when nothing is returned from an '{1}' function.", + correctionMessage: 'Try changing the return type.'); + + AvoidVoidAsync() + : super( + name: 'avoid_void_async', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + var arguments = _arguments(node.declaredElement); + if (arguments != null && + _isVoid(node.returnType) && + node.name.lexeme != 'main') { + rule.reportLintForToken(node.name, arguments: arguments); + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + var arguments = _arguments(node.declaredElement); + if (arguments != null && _isVoid(node.returnType)) { + rule.reportLintForToken(node.name, arguments: arguments); + } + } + + List? _arguments(ExecutableElement? element) { + if (element == null) { + return null; + } else if (element.isAsynchronous) { + return ['Future', 'async']; + } else if (element.isGenerator) { + return ['Stream', 'async*']; + } + return null; + } + + bool _isVoid(TypeAnnotation? typeAnnotation) => + typeAnnotation?.type is VoidType; +} diff --git a/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart b/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart new file mode 100644 index 00000000000..6b690038164 --- /dev/null +++ b/pkg/linter/lib/src/rules/avoid_web_libraries_in_flutter.dart @@ -0,0 +1,136 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:yaml/yaml.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = + r'Avoid using web-only libraries outside Flutter web plugin packages.'; + +const _details = r''' +**AVOID** using web libraries, `dart:html`, `dart:js` and +`dart:js_util` in Flutter packages that are not web plugins. These libraries are +not supported outside a web context; functionality that depends on them will +fail at runtime in Flutter mobile, and their use is generally discouraged in +Flutter web. + +Web library access *is* allowed in: + +* plugin packages that declare `web` as a supported context + +otherwise, imports of `dart:html`, `dart:js` and `dart:js_util` are disallowed. +'''; + +/// todo (pq): consider making a utility and sharing w/ `prefer_relative_imports` +YamlMap _parseYaml(String content) { + try { + var doc = loadYamlNode(content); + if (doc is YamlMap) { + return doc; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // Fall-through. + } + return YamlMap(); +} + +class AvoidWebLibrariesInFlutter extends LintRule { + static const LintCode code = LintCode('avoid_web_libraries_in_flutter', + "Don't use web-only libraries outside Flutter web plugin packages.", + correctionMessage: 'Try finding a different library for your needs.'); + + /// Cache of most recent analysis root to parsed "hasFlutter" state. + static final Map _rootHasFlutterCache = {}; + + AvoidWebLibrariesInFlutter() + : super( + name: 'avoid_web_libraries_in_flutter', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + bool hasFlutterDep(File? pubspec) { + if (pubspec == null) { + return false; + } + + YamlMap parsedPubspec; + try { + var content = pubspec.readAsStringSync(); + parsedPubspec = _parseYaml(content); + // ignore: avoid_catches_without_on_clauses + } catch (_) { + return false; + } + + // If it has Flutter as a dependency, continue checking. + if (parsedPubspec['dependencies'] case {'flutter': var _?}) { + if (parsedPubspec['flutter'] + case {'plugin': {'platforms': {'web': _?}}}) { + // Is a Flutter web plugin, allow web libraries. + return false; + } else { + // Is a non-web Flutter package, don't allow web libraries. + return true; + } + } + + // Is not a Flutter package, allow web libraries. + return false; + } + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + bool hasFlutter(String root) { + var hasFlutter = _rootHasFlutterCache[root]; + if (hasFlutter == null) { + // clear previous cache + _rootHasFlutterCache.clear(); + var pubspecFile = locatePubspecFile(context.currentUnit.unit); + hasFlutter = hasFlutterDep(pubspecFile); + _rootHasFlutterCache[root] = hasFlutter; + } + return hasFlutter; + } + + var root = context.package?.root; + if (root != null) { + if (hasFlutter(root)) { + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isWebUri(String uri) { + var uriLength = uri.length; + return (uriLength == 9 && uri == 'dart:html') || + (uriLength == 7 && uri == 'dart:js') || + (uriLength == 12 && uri == 'dart:js_util'); + } + + @override + void visitImportDirective(ImportDirective node) { + var uriString = node.uri.stringValue; + if (uriString != null && isWebUri(uriString)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/await_only_futures.dart b/pkg/linter/lib/src/rules/await_only_futures.dart new file mode 100644 index 00000000000..c12f97a4f20 --- /dev/null +++ b/pkg/linter/lib/src/rules/await_only_futures.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Await only futures.'; + +const _details = r''' +**AVOID** using await on anything which is not a future. + +Await is allowed on the types: `Future`, `FutureOr`, `Future?`, +`FutureOr?` and `dynamic`. + +Further, using `await null` is specifically allowed as a way to introduce a +microtask delay. + +**BAD:** +```dart +main() async { + print(await 23); +} +``` +**GOOD:** +```dart +main() async { + await null; // If a delay is really intended. + print(23); +} +``` +'''; + +class AwaitOnlyFutures extends LintRule { + static const LintCode code = LintCode('await_only_futures', + "Uses 'await' on an instance of '{0}', which is not a subtype of 'Future'.", + correctionMessage: + "Try removing the 'await' or changing the expression."); + + AwaitOnlyFutures() + : super( + name: 'await_only_futures', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAwaitExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAwaitExpression(AwaitExpression node) { + if (node.expression is NullLiteral) return; + + var type = node.expression.staticType; + if (!(type == null || + type.isDartAsyncFuture || + type is DynamicType || + type is InvalidType || + type.extendsClass('Future', 'dart.async') || + type.implementsInterface('Future', 'dart.async') || + type.isDartAsyncFutureOr)) { + rule.reportLintForToken(node.awaitKeyword, arguments: [type]); + } + } +} diff --git a/pkg/linter/lib/src/rules/camel_case_extensions.dart b/pkg/linter/lib/src/rules/camel_case_extensions.dart new file mode 100644 index 00000000000..29851f47b45 --- /dev/null +++ b/pkg/linter/lib/src/rules/camel_case_extensions.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../utils.dart'; + +const _desc = r'Name extensions using UpperCamelCase.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/style#do-name-extensions-using-uppercamelcase): + +**DO** name extensions using `UpperCamelCase`. + +Extensions should capitalize the first letter of each word (including +the first word), and use no separators. + +**GOOD:** +```dart +extension MyFancyList on List { + // ... +} + +extension SmartIterable on Iterable { + // ... +} +``` +'''; + +class CamelCaseExtensions extends LintRule { + static const LintCode code = LintCode('camel_case_extensions', + "The extension name '{0}' isn't an UpperCamelCase identifier.", + correctionMessage: + 'Try changing the name to follow the UpperCamelCase style.'); + + CamelCaseExtensions() + : super( + name: 'camel_case_extensions', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addExtensionDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + var name = node.name; + if (name != null && !isCamelCase(name.lexeme)) { + rule.reportLintForToken(name, arguments: [name.lexeme]); + } + } +} diff --git a/pkg/linter/lib/src/rules/camel_case_types.dart b/pkg/linter/lib/src/rules/camel_case_types.dart new file mode 100644 index 00000000000..a494a9d2197 --- /dev/null +++ b/pkg/linter/lib/src/rules/camel_case_types.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../utils.dart'; + +const _desc = r'Name types using UpperCamelCase.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/style#do-name-types-using-uppercamelcase): + +**DO** name types using UpperCamelCase. + +Classes and typedefs should capitalize the first letter of each word (including +the first word), and use no separators. + +**GOOD:** +```dart +class SliderMenu { + // ... +} + +class HttpRequest { + // ... +} + +typedef num Adder(num x, num y); +``` + +'''; + +class CamelCaseTypes extends LintRule { + static const LintCode code = LintCode('camel_case_types', + "The type name '{0}' isn't an UpperCamelCase identifier.", + correctionMessage: + 'Try changing the name to follow the UpperCamelCase style.'); + + CamelCaseTypes() + : super( + name: 'camel_case_types', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addGenericTypeAlias(this, visitor); + registry.addClassDeclaration(this, visitor); + registry.addClassTypeAlias(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + registry.addEnumDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void check(Token name) { + var lexeme = name.lexeme; + if (!isCamelCase(lexeme)) { + rule.reportLintForToken(name, arguments: [lexeme]); + } + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + check(node.name); + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + check(node.name); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + check(node.name); + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + check(node.name); + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + check(node.name); + } +} diff --git a/pkg/linter/lib/src/rules/cancel_subscriptions.dart b/pkg/linter/lib/src/rules/cancel_subscriptions.dart new file mode 100644 index 00000000000..031f886a319 --- /dev/null +++ b/pkg/linter/lib/src/rules/cancel_subscriptions.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/leak_detector_visitor.dart'; + +const _desc = r'Cancel instances of dart.async.StreamSubscription.'; + +const _details = r''' +**DO** invoke `cancel` on instances of `dart.async.StreamSubscription`. + +Cancelling instances of StreamSubscription prevents memory leaks and unexpected +behavior. + +**BAD:** +```dart +class A { + StreamSubscription _subscriptionA; // LINT + void init(Stream stream) { + _subscriptionA = stream.listen((_) {}); + } +} +``` + +**BAD:** +```dart +void someFunction() { + StreamSubscription _subscriptionF; // LINT +} +``` + +**GOOD:** +```dart +class B { + StreamSubscription _subscriptionB; // OK + void init(Stream stream) { + _subscriptionB = stream.listen((_) {}); + } + + void dispose(filename) { + _subscriptionB.cancel(); + } +} +``` + +**GOOD:** +```dart +void someFunctionOK() { + StreamSubscription _subscriptionB; // OK + _subscriptionB.cancel(); +} +``` + +'''; + +bool _isSubscription(DartType type) => + type.implementsInterface('StreamSubscription', 'dart.async'); + +class CancelSubscriptions extends LintRule { + static const LintCode code = LintCode( + 'cancel_subscriptions', "Uncancelled instance of 'StreamSubscription'.", + correctionMessage: "Try invoking 'cancel' in the function in which the " + "'StreamSubscription' was created."); + + CancelSubscriptions() + : super( + name: 'cancel_subscriptions', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends LeakDetectorProcessors { + static const _cancelMethodName = 'cancel'; + + @override + Map predicates = { + _isSubscription: _cancelMethodName + }; + + _Visitor(super.rule); +} diff --git a/pkg/linter/lib/src/rules/cascade_invocations.dart b/pkg/linter/lib/src/rules/cascade_invocations.dart new file mode 100644 index 00000000000..9f889a412b1 --- /dev/null +++ b/pkg/linter/lib/src/rules/cascade_invocations.dart @@ -0,0 +1,341 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Cascade consecutive method invocations on the same reference.'; + +const _details = r''' +**DO** Use the cascading style when successively invoking methods on the same +reference. + +**BAD:** +```dart +SomeClass someReference = SomeClass(); +someReference.firstMethod(); +someReference.secondMethod(); +``` + +**BAD:** +```dart +SomeClass someReference = SomeClass(); +... +someReference.firstMethod(); +someReference.aProperty = value; +someReference.secondMethod(); +``` + +**GOOD:** +```dart +SomeClass someReference = SomeClass() + ..firstMethod() + ..aProperty = value + ..secondMethod(); +``` + +**GOOD:** +```dart +SomeClass someReference = SomeClass(); +... +someReference + ..firstMethod() + ..aProperty = value + ..secondMethod(); +``` + +'''; + +Element? _getElementFromVariableDeclarationStatement( + VariableDeclarationStatement statement) { + var variables = statement.variables.variables; + if (variables.length == 1) { + var variable = variables.single; + if (variable.initializer is AwaitExpression) { + // `await` has a higher precedence than a cascade, but because the token + // `await` is followed by whitespace, it may look like the cascade binds + // tighter, for example in `await Future.value([1,2,3])..forEach(print)`. + // + // In such a case, we should not return any cascadable element here. + return null; + } + return variable.declaredElement; + } + return null; +} + +ExecutableElement? _getExecutableElementFromMethodInvocation( + MethodInvocation node) { + if (_isInvokedWithoutNullAwareOperator(node.operator)) { + var executableElement = node.methodName.canonicalElement; + if (executableElement is ExecutableElement) { + return executableElement; + } + } + return null; +} + +Element? _getPrefixElementFromExpression(Expression rawExpression) { + var expression = rawExpression.unParenthesized; + if (expression is PrefixedIdentifier) { + return expression.prefix.canonicalElement; + } else if (expression is PropertyAccess && + _isInvokedWithoutNullAwareOperator(expression.operator) && + expression.target is SimpleIdentifier) { + return expression.target.canonicalElement; + } + return null; +} + +Element? _getTargetElementFromCascadeExpression(CascadeExpression node) => + node.target.canonicalElement; + +Element? _getTargetElementFromMethodInvocation(MethodInvocation node) => + node.target.canonicalElement; + +bool _isInvokedWithoutNullAwareOperator(Token? token) => + token?.type == TokenType.PERIOD; + +/// Rule to lint consecutive invocations of methods or getters on the same +/// reference that could be done with the cascade operator. +class CascadeInvocations extends LintRule { + static const LintCode code = LintCode( + 'cascade_invocations', 'Unnecessary duplication of receiver.', + correctionMessage: 'Try using a cascade to avoid the duplication.'); + + /// Default constructor. + CascadeInvocations() + : super( + name: 'cascade_invocations', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBlock(this, visitor); + } +} + +/// A CascadableExpression is an object that is built from an expression and +/// knows if it is able to join to another CascadableExpression. +class _CascadableExpression { + static final nullCascadableExpression = + _CascadableExpression._internal(null, []); + + /// Whether this expression can be joined with a previous expression via a + /// cascade operation. + /// + /// If this expression is a [PropertyAccess], [CascadeExpression], or + /// [MethodInvocation] in which the target is not a [SimpleIdentifier], or an + /// [AssignmentExpression] in which the left side is not a [SimpleIdentifier], + /// it cannot join. See bugs https://github.com/dart-lang/linter/issues/1323 + /// and https://github.com/dart-lang/linter/issues/3240. + // TODO(https://github.com/dart-lang/linter/issues/3240): Refactor this lint + // rule to use + // DartTypeUtilities.canonicalElementsFromIdentifiersAreEqual(), which + // should remove this need for checking for a simple target. + final bool canJoin; + + /// Whether this expression can receive an additional expression with a + /// cascade operation. + /// + /// For example, `a.b = 1` can receive, but `a = 1` cannot receive. + /// + /// If this expression is a [PropertyAccess], [CascadeExpression], or + /// [MethodInvocation] in which the target is not a [SimpleIdentifier], or an + /// [AssignmentExpression] in which the left side is not a [SimpleIdentifier], + /// it cannot receive. See bugs + /// https://github.com/dart-lang/linter/issues/1323 and + /// https://github.com/dart-lang/linter/issues/3240. + // TODO(https://github.com/dart-lang/linter/issues/3240): Refactor this lint + // rule to use + // DartTypeUtilities.canonicalElementsFromIdentifiersAreEqual(), which + // should remove this need for checking for a simple target. + final bool canReceive; + final bool canBeCascaded; + + /// This is necessary when you have a variable declaration so that element + /// is critical and it can't be used as a parameter of a method invocation or + /// in the right part of an assignment in a following expression that we would + /// like to join to this. + final bool isCritical; + final Element? element; + final List criticalNodes; + + factory _CascadableExpression.fromExpressionStatement( + ExpressionStatement statement) { + var expression = statement.expression.unParenthesized; + if (expression is AssignmentExpression) { + return _CascadableExpression._fromAssignmentExpression(expression); + } + if (expression is MethodInvocation) { + return _CascadableExpression._fromMethodInvocation(expression); + } + if (expression is CascadeExpression) { + return _CascadableExpression._fromCascadeExpression(expression); + } + if (expression is PropertyAccess && + _isInvokedWithoutNullAwareOperator(expression.operator)) { + return _CascadableExpression._fromPropertyAccess(expression); + } + if (expression is PrefixedIdentifier) { + return _CascadableExpression._fromPrefixedIdentifier(expression); + } + return nullCascadableExpression; + } + + factory _CascadableExpression.fromVariableDeclarationStatement( + VariableDeclarationStatement node) { + var element = _getElementFromVariableDeclarationStatement(node); + return _CascadableExpression._internal(element, [], + canReceive: true, isCritical: true); + } + + factory _CascadableExpression._fromAssignmentExpression( + AssignmentExpression node) { + var leftExpression = node.leftHandSide.unParenthesized; + if (leftExpression is SimpleIdentifier) { + return _CascadableExpression._internal( + leftExpression.staticElement?.canonicalElement, [node.rightHandSide], + canReceive: node.operator.type != TokenType.QUESTION_QUESTION_EQ, + isCritical: true); + } + // setters + var variable = _getPrefixElementFromExpression(leftExpression); + var canReceive = node.operator.type != TokenType.QUESTION_QUESTION_EQ && + variable is VariableElement && + !variable.isStatic; + return _CascadableExpression._internal(variable, [node.rightHandSide], + canJoin: true, canReceive: canReceive, canBeCascaded: true); + } + + factory _CascadableExpression._fromCascadeExpression(CascadeExpression node) { + var targetIsSimple = node.target is SimpleIdentifier; + return _CascadableExpression._internal( + _getTargetElementFromCascadeExpression(node), node.cascadeSections, + canJoin: targetIsSimple, + canReceive: targetIsSimple, + canBeCascaded: true); + } + + factory _CascadableExpression._fromMethodInvocation(MethodInvocation node) { + var executableElement = _getExecutableElementFromMethodInvocation(node); + var isNonStatic = executableElement?.isStatic == false; + if (isNonStatic) { + var targetIsSimple = node.target is SimpleIdentifier; + return _CascadableExpression._internal( + _getTargetElementFromMethodInvocation(node), [node.argumentList], + canJoin: targetIsSimple, + canReceive: targetIsSimple, + canBeCascaded: true); + } + return nullCascadableExpression; + } + + factory _CascadableExpression._fromPrefixedIdentifier( + PrefixedIdentifier node) => + _CascadableExpression._internal(node.prefix.canonicalElement, [], + canJoin: true, canReceive: true, canBeCascaded: true); + + factory _CascadableExpression._fromPropertyAccess(PropertyAccess node) { + var targetIsSimple = node.target is SimpleIdentifier; + return _CascadableExpression._internal(node.target.canonicalElement, [], + canJoin: targetIsSimple, + canReceive: targetIsSimple, + canBeCascaded: true); + } + + _CascadableExpression._internal(this.element, this.criticalNodes, + {this.canJoin = false, + this.canReceive = false, + this.canBeCascaded = false, + this.isCritical = false}); + + /// Whether `this` is compatible to be joined with [expressionBox] with a + /// cascade operation. + bool compatibleWith(_CascadableExpression expressionBox) => + element != null && + expressionBox.canReceive && + canJoin && + (canBeCascaded || expressionBox.canBeCascaded) && + element == expressionBox.element && + !_hasCriticalDependencies(expressionBox); + + bool _hasCriticalDependencies(_CascadableExpression expressionBox) { + if (!expressionBox.isCritical) return false; + + for (var node in criticalNodes) { + if (_NodeVisitor(expressionBox).isOrHasCriticalNode(node)) { + return true; + } + } + + return false; + } +} + +class _NodeVisitor extends UnifyingAstVisitor { + final _CascadableExpression expressionBox; + + bool foundCriticalNode = false; + _NodeVisitor(this.expressionBox); + + bool isCriticalNode(AstNode node) => + node.canonicalElement == expressionBox.element; + + bool isOrHasCriticalNode(AstNode node) { + node.accept(this); + return foundCriticalNode; + } + + @override + visitNode(AstNode node) { + if (foundCriticalNode) return; + foundCriticalNode = isCriticalNode(node); + + if (!foundCriticalNode) { + super.visitNode(node); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBlock(Block node) { + if (node.statements.length < 2) { + return; + } + var previousExpressionBox = _CascadableExpression.nullCascadableExpression; + for (var statement in node.statements) { + var currentExpressionBox = _CascadableExpression.nullCascadableExpression; + if (statement is VariableDeclarationStatement) { + currentExpressionBox = + _CascadableExpression.fromVariableDeclarationStatement(statement); + } + if (statement is ExpressionStatement) { + currentExpressionBox = + _CascadableExpression.fromExpressionStatement(statement); + } + if (currentExpressionBox.compatibleWith(previousExpressionBox)) { + rule.reportLint(statement); + } + previousExpressionBox = currentExpressionBox; + } + } +} diff --git a/pkg/linter/lib/src/rules/cast_nullable_to_non_nullable.dart b/pkg/linter/lib/src/rules/cast_nullable_to_non_nullable.dart new file mode 100644 index 00000000000..49b98c96b3a --- /dev/null +++ b/pkg/linter/lib/src/rules/cast_nullable_to_non_nullable.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't cast a nullable value to a non nullable type."; + +const _details = r''' +**DON'T** cast a nullable value to a non nullable type. This hides a null check +and most of the time it is not what is expected. + +**BAD:** +```dart +class A {} +class B extends A {} + +A? a; +var v = a as B; +var v = a as A; +``` + +**GOOD:** +```dart +class A {} +class B extends A {} + +A? a; +var v = a! as B; +var v = a!; +``` + +'''; + +class CastNullableToNonNullable extends LintRule { + static const LintCode code = LintCode('cast_nullable_to_non_nullable', + "Don't cast a nullable value to a non-nullable type.", + correctionMessage: + "Try adding a not-null assertion ('!') to make the type " + 'non-nullable.'); + + CastNullableToNonNullable() + : super( + name: 'cast_nullable_to_non_nullable', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addAsExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitAsExpression(AsExpression node) { + var expressionType = node.expression.staticType; + var type = node.type.type; + if (expressionType is! DynamicType && + expressionType is! InvalidType && + expressionType != null && + context.typeSystem.isNullable(expressionType) && + type != null && + context.typeSystem.isNonNullable(type)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/close_sinks.dart b/pkg/linter/lib/src/rules/close_sinks.dart new file mode 100644 index 00000000000..b71b2d13ced --- /dev/null +++ b/pkg/linter/lib/src/rules/close_sinks.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/leak_detector_visitor.dart'; + +const _desc = r'Close instances of `dart.core.Sink`.'; + +const _details = r''' +**DO** invoke `close` on instances of `dart.core.Sink`. + +Closing instances of Sink prevents memory leaks and unexpected behavior. + +**BAD:** +```dart +class A { + IOSink _sinkA; + void init(filename) { + _sinkA = File(filename).openWrite(); // LINT + } +} +``` + +**BAD:** +```dart +void someFunction() { + IOSink _sinkF; // LINT +} +``` + +**GOOD:** +```dart +class B { + IOSink _sinkB; + void init(filename) { + _sinkB = File(filename).openWrite(); // OK + } + + void dispose(filename) { + _sinkB.close(); + } +} +``` + +**GOOD:** +```dart +void someFunctionOK() { + IOSink _sinkFOK; // OK + _sinkFOK.close(); +} +``` + +'''; + +bool _isSink(DartType type) => type.implementsInterface('Sink', 'dart.core'); + +bool _isSocket(DartType type) => type.implementsInterface('Socket', 'dart.io'); + +class CloseSinks extends LintRule { + static const LintCode code = LintCode( + 'close_sinks', "Unclosed instance of 'Sink'.", + correctionMessage: + "Try invoking 'close' in the function in which the 'Sink' was " + 'created.'); + + CloseSinks() + : super( + name: 'close_sinks', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends LeakDetectorProcessors { + static const _closeMethodName = 'close'; + static const _destroyMethodName = 'destroy'; + + @override + Map predicates = { + _isSink: _closeMethodName, + _isSocket: _destroyMethodName + }; + + _Visitor(super.rule); +} diff --git a/pkg/linter/lib/src/rules/collection_methods_unrelated_type.dart b/pkg/linter/lib/src/rules/collection_methods_unrelated_type.dart new file mode 100644 index 00000000000..d42449905e8 --- /dev/null +++ b/pkg/linter/lib/src/rules/collection_methods_unrelated_type.dart @@ -0,0 +1,154 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; +import '../util/unrelated_types_visitor.dart'; + +const _desc = 'Invocation of various collection methods with arguments of ' + 'unrelated types.'; + +const _details = r''' +**DON'T** invoke certain collection method with an argument with an unrelated +type. + +Doing this will invoke `==` on the collection's elements and most likely will +return `false`. + +An argument passed to a collection method should relate to the collection type +as follows: + +* an argument to `Iterable.contains` should be related to `E` +* an argument to `List.remove` should be related to `E` +* an argument to `Map.containsKey` should be related to `K` +* an argument to `Map.containsValue` should be related to `V` +* an argument to `Map.remove` should be related to `K` +* an argument to `Map.[]` should be related to `K` +* an argument to `Queue.remove` should be related to `E` +* an argument to `Set.lookup` should be related to `E` +* an argument to `Set.remove` should be related to `E` + +**BAD:** +```dart +void someFunction() { + var list = []; + if (list.contains('1')) print('someFunction'); // LINT +} +``` + +**BAD:** +```dart +void someFunction() { + var set = {}; + set.remove('1'); // LINT +} +``` + +**GOOD:** +```dart +void someFunction() { + var list = []; + if (list.contains(1)) print('someFunction'); // OK +} +``` + +**GOOD:** +```dart +void someFunction() { + var set = {}; + set.remove(1); // OK +} +``` + +'''; + +class CollectionMethodsUnrelatedType extends LintRule { + static const LintCode code = LintCode('collection_methods_unrelated_type', + "The argument type '{0}' isn't related to '{1}'."); + + CollectionMethodsUnrelatedType() + : super( + name: 'collection_methods_unrelated_type', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context.typeSystem, context.typeProvider); + registry.addIndexExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends UnrelatedTypesProcessors { + _Visitor(super.rule, super.typeSystem, super.typeProvider); + + @override + List get methods => [ + // Argument to `Iterable.contains` should be assignable to `E`. + MethodDefinitionForElement( + typeProvider.iterableElement, + 'contains', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `List.remove` should be assignable to `E`. + MethodDefinitionForElement( + typeProvider.listElement, + 'remove', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `Map.containsKey` should be assignable to `K`. + MethodDefinitionForElement( + typeProvider.mapElement, + 'containsKey', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `Map.containsValue` should be assignable to `V`. + MethodDefinitionForElement( + typeProvider.mapElement, + 'containsValue', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + typeArgumentIndex: 1, + ), + // Argument to `Map.remove` should be assignable to `K`. + MethodDefinitionForElement( + typeProvider.mapElement, + 'remove', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `Queue.remove` should be assignable to `E`. + MethodDefinitionForName( + 'dart.collection', + 'Queue', + 'remove', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `Set.lookup` should be assignable to `E`. + MethodDefinitionForElement( + typeProvider.setElement, + 'lookup', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + // Argument to `Set.remove` should be assignable to `E`. + MethodDefinitionForElement( + typeProvider.setElement, + 'remove', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + ]; + + @override + List get indexOperators => [ + // Argument to `Map.[]` should be assignable to `K`. + MethodDefinitionForElement( + typeProvider.mapElement, + '[]', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + ]; +} diff --git a/pkg/linter/lib/src/rules/combinators_ordering.dart b/pkg/linter/lib/src/rules/combinators_ordering.dart new file mode 100644 index 00000000000..1579e8dac2c --- /dev/null +++ b/pkg/linter/lib/src/rules/combinators_ordering.dart @@ -0,0 +1,75 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; + +const _desc = r'Sort combinator names alphabetically.'; + +const _details = r''' +**DO** sort combinator names alphabetically. + +**BAD:** +```dart +import 'a.dart' show B, A hide D, C; +export 'a.dart' show B, A hide D, C; +``` + +**GOOD:** +```dart +import 'a.dart' show A, B hide C, D; +export 'a.dart' show A, B hide C, D; +``` + +'''; + +class CombinatorsOrdering extends LintRule { + static const LintCode code = LintCode( + 'combinators_ordering', 'Sort combinator names alphabetically.', + correctionMessage: 'Try sorting the combinator names alphabetically.'); + + CombinatorsOrdering() + : super( + name: 'combinators_ordering', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this); + registry.addHideCombinator(this, visitor); + registry.addShowCombinator(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitHideCombinator(HideCombinator node) { + if (!node.hiddenNames.map((e) => e.name).isSorted()) { + rule.reportLint(node); + } + } + + @override + void visitShowCombinator(ShowCombinator node) { + if (!node.shownNames.map((e) => e.name).isSorted()) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/comment_references.dart b/pkg/linter/lib/src/rules/comment_references.dart new file mode 100644 index 00000000000..2a0bb97565b --- /dev/null +++ b/pkg/linter/lib/src/rules/comment_references.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Only reference in scope identifiers in doc comments.'; + +const _details = r''' +**DO** reference only in scope identifiers in doc comments. + +If you surround things like variable, method, or type names in square brackets, +then [`dart doc`](https://dart.dev/tools/dart-doc) will look +up the name and link to its docs. For this all to work, ensure that all +identifiers in docs wrapped in brackets are in scope. + +For example, assuming `outOfScopeId` is out of scope: + +**BAD:** +```dart +/// Return true if [value] is larger than [outOfScopeId]. +bool isOutOfRange(int value) { ... } +``` + +**GOOD:** +```dart +/// Return the larger of [a] or [b]. +int max_int(int a, int b) { ... } +``` + +Note that the square bracket comment format is designed to allow +comments to refer to declarations using a fairly natural format +but does not allow *arbitrary expressions*. In particular, code +references within square brackets can consist of either + +- a single identifier where the identifier is any identifier in scope for the comment (see the spec for what is in scope in doc comments), +- two identifiers separated by a period where the first identifier is the name of a class that is in scope and the second is the name of a member declared in the class, +- a single identifier followed by a pair of parentheses where the identifier is the name of a class that is in scope (used to refer to the unnamed constructor for the class), or +- two identifiers separated by a period and followed by a pair of parentheses where the first identifier is the name of a class that is in scope and the second is the name of a named constructor (not strictly necessary, but allowed for consistency). + +'''; + +class CommentReferences extends LintRule { + static const LintCode code = LintCode( + 'comment_references', "The referenced name isn't visible in scope.", + correctionMessage: 'Try adding an import for the referenced name.'); + + CommentReferences() + : super( + name: 'comment_references', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addComment(this, visitor); + registry.addCommentReference(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final links = []; + + _Visitor(this.rule); + + @override + void visitComment(Comment node) { + // clear links of previous comments + links.clear(); + + // Check for keywords that are not treated as references by the parser + // but should be flagged by the linter. + // Note that no special care is taken to handle embedded code blocks. + for (var token in node.tokens) { + if (!token.isSynthetic) { + var comment = token.lexeme; + var leftIndex = comment.indexOf('['); + while (leftIndex >= 0) { + var rightIndex = comment.indexOf(']', leftIndex); + if (rightIndex >= 0) { + var reference = comment.substring(leftIndex + 1, rightIndex); + if (_isParserSpecialCase(reference)) { + var nameOffset = token.offset + leftIndex + 1; + rule.reporter.reportErrorForOffset( + rule.lintCode, nameOffset, reference.length); + } + if (rightIndex + 1 < comment.length && + comment[rightIndex + 1] == ':') { + links.add(reference); + } + } + leftIndex = rightIndex < 0 ? -1 : comment.indexOf('[', rightIndex); + } + } + } + } + + @override + void visitCommentReference(CommentReference node) { + var expression = node.expression; + if (expression.isSynthetic) return; + if (expression is Identifier && + expression.staticElement == null && + !links.contains(expression.name)) { + rule.reportLint(expression); + } + } + + bool _isParserSpecialCase(String reference) => + reference == 'this' || + reference == 'null' || + reference == 'true' || + reference == 'false'; +} diff --git a/pkg/linter/lib/src/rules/conditional_uri_does_not_exist.dart b/pkg/linter/lib/src/rules/conditional_uri_does_not_exist.dart new file mode 100644 index 00000000000..c21a7aacd95 --- /dev/null +++ b/pkg/linter/lib/src/rules/conditional_uri_does_not_exist.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Missing conditional import.'; + +const _details = r''' +**DON'T** reference files that do not exist in conditional imports. + +Code may fail at runtime if the condition evaluates such that the missing file +needs to be imported. + +**BAD:** +```dart +import 'file_that_does_exist.dart' + if (condition) 'file_that_does_not_exist.dart'; +``` + +**GOOD:** +```dart +import 'file_that_does_exist.dart' + if (condition) 'file_that_also_does_exist.dart'; +``` + +'''; + +class ConditionalUriDoesNotExist extends LintRule { + static const LintCode code = LintCode('conditional_uri_does_not_exist', + "The target of a conditional import doesn't exist.", + correctionMessage: + 'Try creating the imported file or changing the URI to reference an ' + 'existing file.'); + + ConditionalUriDoesNotExist() + : super( + name: 'conditional_uri_does_not_exist', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConfiguration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + static const LintCode code = LintCode('conditional_uri_does_not_exist', + "The target of the conditional URI '{0}' doesn't exist.", + correctionMessage: 'Try creating the file referenced by the URI, or ' + 'try using a URI for a file that does exist.'); + + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConfiguration(Configuration configuration) { + var uri = configuration.resolvedUri; + if (uri is DirectiveUriWithRelativeUriString) { + var source = uri is DirectiveUriWithSource ? uri.source : null; + // Checking source with .exists() will not detect the presence of overlays + // in the analysis server (although running the script when the files + // don't exist on disk would also fail to find it). + if (!(source?.exists() ?? false)) { + rule.reportLint(configuration.uri, + arguments: [uri.relativeUriString], errorCode: code); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/constant_identifier_names.dart b/pkg/linter/lib/src/rules/constant_identifier_names.dart new file mode 100644 index 00000000000..45ca7a74304 --- /dev/null +++ b/pkg/linter/lib/src/rules/constant_identifier_names.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../utils.dart'; + +const _desc = r'Prefer using lowerCamelCase for constant names.'; + +const _details = r''' +**PREFER** using lowerCamelCase for constant names. + +In new code, use `lowerCamelCase` for constant variables, including enum values. + +In existing code that uses `ALL_CAPS_WITH_UNDERSCORES` for constants, you may +continue to use all caps to stay consistent. + +**BAD:** +```dart +const PI = 3.14; +const kDefaultTimeout = 1000; +final URL_SCHEME = RegExp('^([a-z]+):'); + +class Dice { + static final NUMBER_GENERATOR = Random(); +} +``` + +**GOOD:** +```dart +const pi = 3.14; +const defaultTimeout = 1000; +final urlScheme = RegExp('^([a-z]+):'); + +class Dice { + static final numberGenerator = Random(); +} +``` + +'''; + +class ConstantIdentifierNames extends LintRule { + static const LintCode code = LintCode('constant_identifier_names', + "The constant name '{0}' isn't a lowerCamelCase identifier.", + correctionMessage: + 'Try changing the name to follow the lowerCamelCase style.'); + + ConstantIdentifierNames() + : super( + name: 'constant_identifier_names', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDeclaredVariablePattern(this, visitor); + registry.addEnumConstantDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addVariableDeclarationList(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkIdentifier(Token id) { + var name = id.lexeme; + if (!isLowerCamelCase(name)) { + rule.reportLintForToken(id, arguments: [name]); + } + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + if (node.parent.isFieldNameShortcut) return; + checkIdentifier(node.name); + } + + @override + void visitEnumConstantDeclaration(EnumConstantDeclaration node) { + checkIdentifier(node.name); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + visitVariableDeclarationList(node.variables); + } + + @override + void visitVariableDeclarationList(VariableDeclarationList node) { + for (var v in node.variables) { + if (v.isConst) { + checkIdentifier(v.name); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/control_flow_in_finally.dart b/pkg/linter/lib/src/rules/control_flow_in_finally.dart new file mode 100755 index 00000000000..39db4a2679c --- /dev/null +++ b/pkg/linter/lib/src/rules/control_flow_in_finally.dart @@ -0,0 +1,177 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid control flow in finally blocks.'; + +const _details = r''' +**AVOID** control flow leaving finally blocks. + +Using control flow in finally blocks will inevitably cause unexpected behavior +that is hard to debug. + +**BAD:** +```dart +class BadReturn { + double nonCompliantMethod() { + try { + return 1 / 0; + } catch (e) { + print(e); + } finally { + return 1.0; // LINT + } + } +} +``` + +**BAD:** +```dart +class BadContinue { + double nonCompliantMethod() { + for (var o in [1, 2]) { + try { + print(o / 0); + } catch (e) { + print(e); + } finally { + continue; // LINT + } + } + return 1.0; + } +} +``` + +**BAD:** +```dart +class BadBreak { + double nonCompliantMethod() { + for (var o in [1, 2]) { + try { + print(o / 0); + } catch (e) { + print(e); + } finally { + break; // LINT + } + } + return 1.0; + } +} +``` + +**GOOD:** +```dart +class Ok { + double compliantMethod() { + var i = 5; + try { + i = 1 / 0; + } catch (e) { + print(e); // OK + } + return i; + } +} +``` + +'''; + +class ControlFlowInFinally extends LintRule { + static const LintCode code = LintCode( + 'control_flow_in_finally', "Use of '{0}' in a 'finally' clause.", + correctionMessage: 'Try restructuring the code.'); + + ControlFlowInFinally() + : super( + name: 'control_flow_in_finally', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBreakStatement(this, visitor); + registry.addContinueStatement(this, visitor); + registry.addReturnStatement(this, visitor); + } +} + +/// Do not extend this class, it is meant to be used from +/// [ControlFlowInFinally] which is in a separate rule to allow a more granular +/// configurability given that reporting throw statements in a finally clause is +/// controversial. +mixin ControlFlowInFinallyBlockReporter { + LintRule get rule; + + void reportIfFinallyAncestorExists(AstNode node, + {required String kind, AstNode? ancestor}) { + var tryStatement = node.thisOrAncestorOfType(); + var finallyBlock = tryStatement?.finallyBlock; + bool finallyBlockAncestorPredicate(AstNode n) => n == finallyBlock; + if (tryStatement == null || + finallyBlock == null || + node.thisOrAncestorMatching(finallyBlockAncestorPredicate) == null) { + return; + } + + var enablerNode = _findEnablerNode( + ancestor, finallyBlockAncestorPredicate, node, tryStatement); + if (enablerNode == null) { + rule.reportLint(node, arguments: [kind]); + } + } + + AstNode? _findEnablerNode( + AstNode? ancestor, + bool Function(AstNode n) finallyBlockAncestorPredicate, + AstNode node, + TryStatement tryStatement) { + AstNode? enablerNode; + if (ancestor == null) { + bool functionBlockPredicate(n) => + n is FunctionBody && + n.thisOrAncestorMatching(finallyBlockAncestorPredicate) != null; + enablerNode = node.thisOrAncestorMatching(functionBlockPredicate); + } else { + enablerNode = ancestor.thisOrAncestorMatching((n) => n == tryStatement); + } + + return enablerNode; + } +} + +class _Visitor extends SimpleAstVisitor + with ControlFlowInFinallyBlockReporter { + @override + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBreakStatement(BreakStatement node) { + reportIfFinallyAncestorExists(node, ancestor: node.target, kind: 'break'); + } + + @override + void visitContinueStatement(ContinueStatement node) { + reportIfFinallyAncestorExists(node, + ancestor: node.target, kind: 'continue'); + } + + @override + void visitReturnStatement(ReturnStatement node) { + reportIfFinallyAncestorExists(node, kind: 'return'); + } +} diff --git a/pkg/linter/lib/src/rules/curly_braces_in_flow_control_structures.dart b/pkg/linter/lib/src/rules/curly_braces_in_flow_control_structures.dart new file mode 100644 index 00000000000..184a013d3d6 --- /dev/null +++ b/pkg/linter/lib/src/rules/curly_braces_in_flow_control_structures.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'DO use curly braces for all flow control structures.'; + +const _details = r''' +**DO** use curly braces for all flow control structures. + +Doing so avoids the [dangling else](https://en.wikipedia.org/wiki/Dangling_else) +problem. + +**BAD:** +```dart +if (overflowChars != other.overflowChars) + return overflowChars < other.overflowChars; +``` + +**GOOD:** +```dart +if (isWeekDay) { + print('Bike to work!'); +} else { + print('Go dancing or read a book!'); +} +``` + +There is one exception to this: an `if` statement with no `else` clause where +the entire `if` statement and the then body all fit in one line. In that case, +you may leave off the braces if you prefer: + +**GOOD:** +```dart +if (arg == null) return defaultValue; +``` + +If the body wraps to the next line, though, use braces: + +**GOOD:** +```dart +if (overflowChars != other.overflowChars) { + return overflowChars < other.overflowChars; +} +``` + +'''; + +class CurlyBracesInFlowControlStructures extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'curly_braces_in_flow_control_structures', + 'Statements in {0} should be enclosed in a block.', + correctionMessage: 'Try wrapping the statement in a block.'); + + CurlyBracesInFlowControlStructures() + : super( + name: 'curly_braces_in_flow_control_structures', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDoStatement(this, visitor); + registry.addForStatement(this, visitor); + registry.addIfStatement(this, visitor); + registry.addWhileStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitDoStatement(DoStatement node) { + _check('a do', node.body); + } + + @override + void visitForStatement(ForStatement node) { + _check('a for', node.body); + } + + @override + void visitIfStatement(IfStatement node) { + var elseStatement = node.elseStatement; + if (elseStatement == null) { + var parent = node.parent; + if (parent is IfStatement && node == parent.elseStatement) { + _check('an if', node.thenStatement); + return; + } + if (node.thenStatement is Block) return; + + var unit = node.root as CompilationUnit; + var lineInfo = unit.lineInfo; + if (lineInfo.getLocation(node.rightParenthesis.end).lineNumber != + lineInfo.getLocation(node.thenStatement.end).lineNumber) { + rule.reportLint(node.thenStatement, arguments: ['an if']); + } + } else { + _check('an if', node.thenStatement); + if (elseStatement is! IfStatement) { + _check('an if', elseStatement); + } + } + } + + @override + void visitWhileStatement(WhileStatement node) { + _check('a while', node.body); + } + + void _check(String where, Statement node) { + if (node is! Block) rule.reportLint(node, arguments: [where]); + } +} diff --git a/pkg/linter/lib/src/rules/dangling_library_doc_comments.dart b/pkg/linter/lib/src/rules/dangling_library_doc_comments.dart new file mode 100644 index 00000000000..8f47facb693 --- /dev/null +++ b/pkg/linter/lib/src/rules/dangling_library_doc_comments.dart @@ -0,0 +1,173 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Attach library doc comments to library directives.'; + +const _details = r''' +Attach library doc comments (with `///`) to library directives, rather than +leaving them dangling near the top of a library. + +**BAD:** +```dart +/// This is a great library. +import 'package:math'; +``` + +```dart +/// This is a great library. + +class C {} +``` + +**GOOD:** +```dart +/// This is a great library. +library; + +import 'package:math'; + +class C {} +``` + +**NOTE:** An unnamed library, like `library;` above, is only supported in Dart +2.19 and later. Code which might run in earlier versions of Dart will need to +provide a name in the `library` directive. +'''; + +class DanglingLibraryDocComments extends LintRule { + static const LintCode code = LintCode( + 'dangling_library_doc_comments', 'Dangling library doc comment.', + correctionMessage: + "Add a 'library' directive after the library comment."); + + DanglingLibraryDocComments() + : super( + name: 'dangling_library_doc_comments', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final DanglingLibraryDocComments rule; + + _Visitor(this.rule); + + @override + void visitCompilationUnit(CompilationUnit node) { + if (node.directives.isNotEmpty) { + // Only consider a doc comment on the first directive. Doc comments on + // other directives do not have the appearance of documenting the library. + var firstDirective = node.directives.first; + if (firstDirective is LibraryDirective) { + // Given the presence of library directive, don't worry about later doc + // comments in the library. + return; + } + if (firstDirective is PartOfDirective) { + // Don't worry about possible "library doc comments" in a part. + return; + } + + var docComment = firstDirective.documentationComment; + if (docComment != null) { + rule.reportLintForToken(docComment.beginToken); + return; + } + + return; + } + + if (node.declarations.isEmpty) { + // Without any declarations, we only need to check for a doc comment as + // the last thing in a file. + Token? endComment = node.endToken.precedingComments; + while (endComment is CommentToken) { + if (endComment is DocumentationCommentToken) { + rule.reportLintForToken(endComment); + } + endComment = endComment.next; + } + return; + } + + var firstDeclaration = node.declarations.first; + var docComment = firstDeclaration.documentationComment; + if (docComment == null) { + return; + } + var lineInfo = node.lineInfo; + + if (docComment.tokens.length > 1) { + for (var i = 0; i < docComment.tokens.length - 1; i++) { + var commentToken = docComment.tokens[i]; + var followingCommentToken = docComment.tokens[i + 1]; + var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber; + var followingCommentLine = + lineInfo.getLocation(followingCommentToken.offset).lineNumber; + if (followingCommentLine > commentEndLine + 1) { + // There is a blank line within the declaration's doc comments. + rule.reportLintForToken(commentToken); + return; + } + } + } + + // We must walk through the comments following the doc comment, tracking + // pairs of consecutive comments so as to check whether any two are + // separated by a blank line. + var commentToken = docComment.endToken; + var followingCommentToken = commentToken.next; + while (followingCommentToken != null) { + // Any blank line between the doc comment and following comments makes + // the doc comment look dangling. + var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber; + var followingCommentLine = + lineInfo.getLocation(followingCommentToken.offset).lineNumber; + if (followingCommentLine > commentEndLine + 1) { + // There is a blank line between the declaration's doc comment and the + // declaration. + rule.reportLint(docComment); + return; + } + + commentToken = followingCommentToken; + followingCommentToken = followingCommentToken.next; + } + + var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber; + // The syntactic entity to which a comment is "attached" is the + // [Comment]'s `parent`, not it's `endToken`'s `next` [Token]. + var tokenAfterDocComment = + (docComment.endToken as DocumentationCommentToken).parent; + if (tokenAfterDocComment == null) { + // We shouldn't get here as the doc comment is attached to + // [firstDeclaration]. + return; + } + var declarationStartLine = + lineInfo.getLocation(tokenAfterDocComment.offset).lineNumber; + if (declarationStartLine > commentEndLine + 1) { + // There is a blank line between the declaration's doc comment and the + // declaration. + rule.reportLint(docComment); + } + } +} diff --git a/pkg/linter/lib/src/rules/deprecated_consistency.dart b/pkg/linter/lib/src/rules/deprecated_consistency.dart new file mode 100644 index 00000000000..f7a7bf387e8 --- /dev/null +++ b/pkg/linter/lib/src/rules/deprecated_consistency.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Missing deprecated annotation.'; + +const _details = r''' +**DO** apply `@Deprecated()` consistently: + +- if a class is deprecated, its constructors should also be deprecated. +- if a field is deprecated, the constructor parameter pointing to it should also + be deprecated. +- if a constructor parameter pointing to a field is deprecated, the field should + also be deprecated. + +**BAD:** +```dart +@deprecated +class A { + A(); +} + +class B { + B({this.field}); + @deprecated + Object field; +} +``` + +**GOOD:** +```dart +@deprecated +class A { + @deprecated + A(); +} + +class B { + B({@deprecated this.field}); + @deprecated + Object field; +} + +class C extends B { + C({@deprecated super.field}); +} +``` +'''; + +class DeprecatedConsistency extends LintRule { + static const LintCode constructorCode = LintCode('deprecated_consistency', + 'Constructors in a deprecated class should be deprecated.', + correctionMessage: 'Try marking the constructor as deprecated.'); + + static const LintCode parameterCode = LintCode('deprecated_consistency', + 'Parameters that initialize a deprecated field should be deprecated.', + correctionMessage: 'Try marking the parameter as deprecated.'); + + static const LintCode fieldCode = LintCode( + 'deprecated_consistency', + 'Fields that are initialized by a deprecated parameter should be ' + 'deprecated.', + correctionMessage: 'Try marking the field as deprecated.'); + + DeprecatedConsistency() + : super( + name: 'deprecated_consistency', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + List get lintCodes => [constructorCode, parameterCode, fieldCode]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addFieldFormalParameter(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var constructorElement = node.declaredElement; + if (constructorElement != null && + constructorElement.enclosingElement.hasDeprecated && + !constructorElement.hasDeprecated) { + rule.reportLint(node, errorCode: DeprecatedConsistency.constructorCode); + } + } + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + var declaredElement = node.declaredElement; + if (declaredElement is! FieldFormalParameterElement) return; + + var field = declaredElement.field; + if (field == null) return; + + if (field.hasDeprecated && !declaredElement.hasDeprecated) { + rule.reportLint(node, errorCode: DeprecatedConsistency.fieldCode); + } + if (!field.hasDeprecated && declaredElement.hasDeprecated) { + rule.reportLintForOffset(field.nameOffset, field.nameLength, + errorCode: DeprecatedConsistency.parameterCode); + } + } +} diff --git a/pkg/linter/lib/src/rules/deprecated_member_use_from_same_package.dart b/pkg/linter/lib/src/rules/deprecated_member_use_from_same_package.dart new file mode 100644 index 00000000000..fdc373fcc3f --- /dev/null +++ b/pkg/linter/lib/src/rules/deprecated_member_use_from_same_package.dart @@ -0,0 +1,473 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/error/deprecated_member_use_verifier.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/workspace/workspace.dart'; + +import '../analyzer.dart'; + +const _desc = + 'Avoid using deprecated elements from within the package in which they are ' + 'declared.'; + +const _details = r''' +Elements that are annotated with `@Deprecated` should not be referenced from +within the package in which they are declared. + +**AVOID** using deprecated elements. + +... + +**BAD:** +```dart +// Declared in one library: +class Foo { + @Deprecated("Use 'm2' instead") + void m1() {} + + void m2({ + @Deprecated('This is an old parameter') int? p, + }) +} + +@Deprecated('Do not use') +int x = 0; + +// In the same or another library, but within the same package: +void m(Foo foo) { + foo.m1(); + foo.m2(p: 7); + x = 1; +} +``` + +Deprecated elements can be used from within _other_ deprecated elements, in +order to allow for the deprecation of a collection of APIs together as one unit. + +**GOOD:** +```dart +// Declared in one library: +class Foo { + @Deprecated("Use 'm2' instead") + void m1() {} + + void m2({ + @Deprecated('This is an old parameter') int? p, + }) +} + +@Deprecated('Do not use') +int x = 0; + +// In the same or another library, but within the same package: +@Deprecated('Do not use') +void m(Foo foo) { + foo.m1(); + foo.m2(p: 7); + x = 1; +} +``` +'''; + +class DeprecatedMemberUseFromSamePackage extends LintRule { + static const LintCode code = LintCode( + 'deprecated_member_use_from_same_package', + "'{0}' is deprecated and shouldn't be used.", + correctionMessage: + 'Try replacing the use of the deprecated member with the replacement, ' + 'if a replacement is specified.', + ); + + static const LintCode codeWithMessage = LintCode( + 'deprecated_member_use_from_same_package', + "'{0}' is deprecated and shouldn't be used. {1}", + correctionMessage: + 'Try replacing the use of the deprecated member with the replacement, ' + 'if a replacement is specified.', + uniqueName: 'LintCode.deprecated_member_use_from_same_package_with_message', + ); + + DeprecatedMemberUseFromSamePackage() + : super( + name: 'deprecated_member_use_from_same_package', + description: _desc, + details: _details, + group: Group.errors); + + @override + List get lintCodes => [code, codeWithMessage]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _DeprecatedMemberUseVerifier extends BaseDeprecatedMemberUseVerifier { + final LintRule _rule; + final WorkspacePackage _workspacePackage; + + _DeprecatedMemberUseVerifier(this._rule, this._workspacePackage); + + @override + void reportError(SyntacticEntity errorEntity, Element element, + String displayName, String? message) { + var library = element is LibraryElement ? element : element.library; + if (library == null || !_workspacePackage.contains(library.source)) { + // In this case, `DEPRECATED_MEMBER_USE` is reported by the analyzer. + return; + } + + var normalizedMessage = message?.trim(); + if (normalizedMessage == null || + normalizedMessage.isEmpty || + normalizedMessage == '.') { + _rule.reportLintForOffset( + errorEntity.offset, + errorEntity.length, + arguments: [displayName], + errorCode: DeprecatedMemberUseFromSamePackage.code, + ); + } else { + if (!normalizedMessage.endsWith('.') && + !normalizedMessage.endsWith('?') && + !normalizedMessage.endsWith('!')) { + normalizedMessage = '$message.'; + } + _rule.reportLintForOffset( + errorEntity.offset, + errorEntity.length, + arguments: [displayName, normalizedMessage], + errorCode: DeprecatedMemberUseFromSamePackage.codeWithMessage, + ); + } + } +} + +/// This visitor uses a [DeprecatedMemberUseVerifier] to both report uses of +/// deprecated elements, and to track the deprecated-ness of ancestor +/// declaration nodes. +class _RecursiveVisitor extends RecursiveAstVisitor { + final _DeprecatedMemberUseVerifier _deprecatedVerifier; + + _RecursiveVisitor(LintRule rule, WorkspacePackage package) + : _deprecatedVerifier = _DeprecatedMemberUseVerifier(rule, package); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _deprecatedVerifier.assignmentExpression(node); + super.visitAssignmentExpression(node); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + _deprecatedVerifier.binaryExpression(node); + super.visitBinaryExpression(node); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitClassDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitClassTypeAlias(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitCompilationUnit(CompilationUnit node) { + var library = node.declaredElement?.library; + if (library == null) { + return; + } + _deprecatedVerifier.pushInDeprecatedValue(library.hasDeprecated); + + super.visitCompilationUnit(node); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitConstructorDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitConstructorName(ConstructorName node) { + _deprecatedVerifier.constructorName(node); + super.visitConstructorName(node); + } + + @override + void visitDefaultFormalParameter(DefaultFormalParameter node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitDefaultFormalParameter(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitEnumDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitExportDirective(ExportDirective node) { + _deprecatedVerifier.exportDirective(node); + super.visitExportDirective(node); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitExtensionDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitExtensionOverride(ExtensionOverride node) { + _deprecatedVerifier.extensionOverride(node); + super.visitExtensionOverride(node); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + _deprecatedVerifier.pushInDeprecatedMetadata(node.metadata); + + try { + super.visitFieldDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitFieldFormalParameter(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitFunctionDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + _deprecatedVerifier.functionExpressionInvocation(node); + super.visitFunctionExpressionInvocation(node); + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitFunctionTypeAlias(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitGenericTypeAlias(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitImportDirective(ImportDirective node) { + _deprecatedVerifier.importDirective(node); + super.visitImportDirective(node); + } + + @override + void visitIndexExpression(IndexExpression node) { + _deprecatedVerifier.indexExpression(node); + super.visitIndexExpression(node); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + _deprecatedVerifier.instanceCreationExpression(node); + super.visitInstanceCreationExpression(node); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitMethodDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + _deprecatedVerifier.methodInvocation(node); + super.visitMethodInvocation(node); + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitMixinDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitNamedType(NamedType node) { + _deprecatedVerifier.namedType(node); + super.visitNamedType(node); + } + + @override + void visitPostfixExpression(PostfixExpression node) { + _deprecatedVerifier.postfixExpression(node); + super.visitPostfixExpression(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + _deprecatedVerifier.prefixExpression(node); + super.visitPrefixExpression(node); + } + + @override + void visitRedirectingConstructorInvocation( + RedirectingConstructorInvocation node) { + _deprecatedVerifier.redirectingConstructorInvocation(node); + super.visitRedirectingConstructorInvocation(node); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter node) { + _deprecatedVerifier + .pushInDeprecatedValue(node.declaredElement?.hasDeprecated ?? false); + + try { + super.visitSimpleFormalParameter(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + _deprecatedVerifier.simpleIdentifier(node); + super.visitSimpleIdentifier(node); + } + + @override + void visitSuperConstructorInvocation(SuperConstructorInvocation node) { + _deprecatedVerifier.superConstructorInvocation(node); + super.visitSuperConstructorInvocation(node); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + _deprecatedVerifier.pushInDeprecatedMetadata(node.metadata); + + try { + super.visitTopLevelVariableDeclaration(node); + } finally { + _deprecatedVerifier.popInDeprecated(); + } + } +} + +/// This [SimpleAstVisitor] visits the [CompilationUnit], and forwards the +/// remainder of visitations to [_RecursiveVisitor], which keeps track of +/// the deprecated-ness of ancestor declaration nodes. +class _Visitor extends SimpleAstVisitor { + final LintRule _rule; + final LinterContext _context; + + _Visitor(this._rule, this._context); + + @override + void visitCompilationUnit(CompilationUnit node) { + var package = _context.package; + if (package == null) { + // If we don't appear to be in any known package structure, then we can + // never report that a deprecated use is from the same package as the + // declaration. + return; + } + var visitor = _RecursiveVisitor(_rule, package); + node.accept(visitor); + } +} diff --git a/pkg/linter/lib/src/rules/diagnostic_describe_all_properties.dart b/pkg/linter/lib/src/rules/diagnostic_describe_all_properties.dart new file mode 100644 index 00000000000..e4ccdb56b98 --- /dev/null +++ b/pkg/linter/lib/src/rules/diagnostic_describe_all_properties.dart @@ -0,0 +1,205 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'DO reference all public properties in debug methods.'; + +const _details = r''' +**DO** reference all public properties in `debug` method implementations. + +Implementers of `Diagnosticable` should reference all public properties in +a `debugFillProperties(...)` or `debugDescribeChildren(...)` method +implementation to improve debuggability at runtime. + +Public properties are defined as fields and getters that are + +* not package-private (e.g., prefixed with `_`) +* not `static` or overriding +* not themselves `Widget`s or collections of `Widget`s + +In addition, the "debug" prefix is treated specially for properties in Flutter. +For the purposes of diagnostics, a property `foo` and a prefixed property +`debugFoo` are treated as effectively describing the same property and it is +sufficient to refer to one or the other. + +**BAD:** +```dart +class Absorber extends Widget { + bool get absorbing => _absorbing; + bool _absorbing; + bool get ignoringSemantics => _ignoringSemantics; + bool _ignoringSemantics; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('absorbing', absorbing)); + // Missing reference to ignoringSemantics + } +} +``` + +**GOOD:** +```dart +class Absorber extends Widget { + bool get absorbing => _absorbing; + bool _absorbing; + bool get ignoringSemantics => _ignoringSemantics; + bool _ignoringSemantics; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('absorbing', absorbing)); + properties.add(DiagnosticsProperty('ignoringSemantics', ignoringSemantics)); + } +} +``` +'''; + +class DiagnosticDescribeAllProperties extends LintRule { + static const LintCode code = LintCode( + 'diagnostic_describe_all_properties', + "The public property isn't described by either 'debugFillProperties' or " + "'debugDescribeChildren'.", + correctionMessage: 'Try describing the property.'); + + DiagnosticDescribeAllProperties() + : super( + name: 'diagnostic_describe_all_properties', + description: _desc, + details: _details, + group: Group.errors, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + } +} + +class _IdentifierVisitor extends RecursiveAstVisitor { + final List properties; + _IdentifierVisitor(this.properties); + + @override + visitSimpleIdentifier(SimpleIdentifier node) { + String debugName; + String name; + const debugPrefix = 'debug'; + if (node.name.startsWith(debugPrefix) && + node.name.length > debugPrefix.length) { + debugName = node.name; + name = '${node.name[debugPrefix.length].toLowerCase()}' + '${node.name.substring(debugPrefix.length + 1)}'; + } else { + name = node.name; + debugName = + '$debugPrefix${node.name[0].toUpperCase()}${node.name.substring(1)}'; + } + properties.removeWhere( + (property) => property.lexeme == debugName || property.lexeme == name); + + super.visitSimpleIdentifier(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + void removeReferences(MethodDeclaration? method, List properties) { + method?.body.accept(_IdentifierVisitor(properties)); + } + + bool skipForDiagnostic({Element? element, DartType? type, Token? name}) => + isPrivate(name) || _isOverridingMember(element) || isWidgetProperty(type); + + @override + void visitClassDeclaration(ClassDeclaration node) { + // We only care about Diagnosticables. + var type = node.declaredElement?.thisType; + if (!type.implementsInterface('Diagnosticable', '')) { + return; + } + + var properties = []; + for (var member in node.members) { + if (member is MethodDeclaration && member.isGetter) { + if (!member.isStatic && + !skipForDiagnostic( + element: member.declaredElement, + name: member.name, + type: member.returnType?.type, + )) { + properties.add(member.name); + } + } else if (member is FieldDeclaration) { + for (var v in member.fields.variables) { + var declaredElement = v.declaredElement; + if (declaredElement != null && + !declaredElement.isStatic && + !skipForDiagnostic( + element: declaredElement, + name: v.name, + type: declaredElement.type, + )) { + properties.add(v.name); + } + } + } + } + + if (properties.isEmpty) { + return; + } + + var debugFillProperties = node.members.getMethod('debugFillProperties'); + var debugDescribeChildren = node.members.getMethod('debugDescribeChildren'); + + // Remove any defined in debugFillProperties. + removeReferences(debugFillProperties, properties); + + // Remove any defined in debugDescribeChildren. + removeReferences(debugDescribeChildren, properties); + + // Flag the rest. + properties.forEach(rule.reportLintForToken); + } + + bool _isOverridingMember(Element? member) { + if (member == null) { + return false; + } + + var classElement = member.thisOrAncestorOfType(); + if (classElement == null) { + return false; + } + var name = member.name; + if (name == null) { + return false; + } + + var libraryUri = classElement.library.source.uri; + return context.inheritanceManager + .getInherited(classElement.thisType, Name(libraryUri, name)) != + null; + } +} diff --git a/pkg/linter/lib/src/rules/directives_ordering.dart b/pkg/linter/lib/src/rules/directives_ordering.dart new file mode 100644 index 00000000000..0208dd6ce3d --- /dev/null +++ b/pkg/linter/lib/src/rules/directives_ordering.dart @@ -0,0 +1,392 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Adhere to Effective Dart Guide directives sorting conventions.'; +const _details = r''' +**DO** follow the directive ordering conventions in +[Effective Dart](https://dart.dev/effective-dart/style#ordering): + +**DO** place `dart:` imports before other imports. + +**BAD:** +```dart +import 'package:bar/bar.dart'; +import 'package:foo/foo.dart'; + +import 'dart:async'; // LINT +import 'dart:html'; // LINT +``` + +**BAD:** +```dart +import 'dart:html'; // OK +import 'package:bar/bar.dart'; + +import 'dart:async'; // LINT +import 'package:foo/foo.dart'; +``` + +**GOOD:** +```dart +import 'dart:async'; // OK +import 'dart:html'; // OK + +import 'package:bar/bar.dart'; +import 'package:foo/foo.dart'; +``` + +**DO** place `package:` imports before relative imports. + +**BAD:** +```dart +import 'a.dart'; +import 'b.dart'; + +import 'package:bar/bar.dart'; // LINT +import 'package:foo/foo.dart'; // LINT +``` + +**BAD:** +```dart +import 'package:bar/bar.dart'; // OK +import 'a.dart'; + +import 'package:foo/foo.dart'; // LINT +import 'b.dart'; +``` + +**GOOD:** +```dart +import 'package:bar/bar.dart'; // OK +import 'package:foo/foo.dart'; // OK + +import 'a.dart'; +import 'b.dart'; +``` + +**DO** specify exports in a separate section after all imports. + +**BAD:** +```dart +import 'src/error.dart'; +export 'src/error.dart'; // LINT +import 'src/string_source.dart'; +``` + +**GOOD:** +```dart +import 'src/error.dart'; +import 'src/string_source.dart'; + +export 'src/error.dart'; // OK +``` + +**DO** sort sections alphabetically. + +**BAD:** +```dart +import 'package:foo/bar.dart'; // OK +import 'package:bar/bar.dart'; // LINT + +import 'a/b.dart'; // OK +import 'a.dart'; // LINT +``` + +**GOOD:** +```dart +import 'package:bar/bar.dart'; // OK +import 'package:foo/bar.dart'; // OK + +import 'a.dart'; // OK +import 'a/b.dart'; // OK +``` +'''; + +const _exportKeyword = 'export'; + +const _importKeyword = 'import'; + +/// Compares directives by package then file in package. +/// +/// Package is everything until the first `/`. +int compareDirectives(String a, String b) { + if (!a.startsWith('package:') || !b.startsWith('package:')) { + return a.compareTo(b); + } + var indexA = a.indexOf('/'); + var indexB = b.indexOf('/'); + if (indexA == -1 || indexB == -1) return a.compareTo(b); + var result = a.substring(0, indexA).compareTo(b.substring(0, indexB)); + if (result != 0) return result; + return a.substring(indexA + 1).compareTo(b.substring(indexB + 1)); +} + +bool _isAbsoluteDirective(NamespaceDirective node) { + var uriContent = node.uri.stringValue; + return uriContent != null && uriContent.contains(':'); +} + +bool _isDartDirective(NamespaceDirective node) { + var uriContent = node.uri.stringValue; + return uriContent != null && uriContent.startsWith('dart:'); +} + +bool _isExportDirective(Directive node) => node is ExportDirective; + +bool _isNotDartDirective(NamespaceDirective node) => !_isDartDirective(node); + +bool _isPackageDirective(NamespaceDirective node) { + var uriContent = node.uri.stringValue; + return uriContent != null && uriContent.startsWith('package:'); +} + +bool _isPartDirective(Directive node) => node is PartDirective; + +bool _isRelativeDirective(NamespaceDirective node) => + !_isAbsoluteDirective(node); + +class DirectivesOrdering extends LintRule { + static const LintCode dartDirectiveGoFirst = LintCode( + 'directives_ordering', "Place 'dart:' {0}s before other {0}s.", + correctionMessage: 'Try sorting the directives.'); + + static const LintCode directiveSectionOrderedAlphabetically = LintCode( + 'directives_ordering', 'Sort directive sections alphabetically.', + correctionMessage: 'Try sorting the directives.'); + + static const LintCode exportDirectiveAfterImportDirectives = LintCode( + 'directives_ordering', + 'Specify exports in a separate section after all imports.', + correctionMessage: 'Try sorting the directives.'); + + static const LintCode packageDirectiveBeforeRelative = LintCode( + 'directives_ordering', "Place 'package:' {0}s before relative {0}s.", + correctionMessage: 'Try sorting the directives.'); + + DirectivesOrdering() + : super( + name: 'directives_ordering', + description: _desc, + details: _details, + group: Group.style); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + } + + void _reportLintWithDartDirectiveGoFirstMessage(AstNode node, String type) { + reportLint(node, + errorCode: DirectivesOrdering.dartDirectiveGoFirst, arguments: [type]); + } + + void _reportLintWithDirectiveSectionOrderedAlphabeticallyMessage( + AstNode node) { + reportLint(node, + errorCode: DirectivesOrdering.directiveSectionOrderedAlphabetically); + } + + void _reportLintWithExportDirectiveAfterImportDirectiveMessage(AstNode node) { + reportLint(node, + errorCode: DirectivesOrdering.exportDirectiveAfterImportDirectives); + } + + void _reportLintWithPackageDirectiveBeforeRelativeMessage( + AstNode node, String type) { + reportLint(node, + errorCode: DirectivesOrdering.packageDirectiveBeforeRelative, + arguments: [type]); + } +} + +// ignore: unused_element +class _PackageBox { + final String _packageName; + + _PackageBox(this._packageName); + + // ignore: unused_element + bool _isNotOwnPackageDirective(NamespaceDirective node) => + _isPackageDirective(node) && !_isOwnPackageDirective(node); + + bool _isOwnPackageDirective(NamespaceDirective node) { + var uriContent = node.uri.stringValue; + return uriContent != null && + uriContent.startsWith('package:$_packageName/'); + } +} + +class _Visitor extends SimpleAstVisitor { + final DirectivesOrdering rule; + + _Visitor(this.rule); + + @override + void visitCompilationUnit(CompilationUnit node) { + var lintedNodes = {}; + _checkDartDirectiveGoFirst(lintedNodes, node); + _checkPackageDirectiveBeforeRelative(lintedNodes, node); + _checkExportDirectiveAfterImportDirective(lintedNodes, node); + _checkDirectiveSectionOrderedAlphabetically(lintedNodes, node); + } + + void _checkDartDirectiveGoFirst( + Set lintedNodes, CompilationUnit node) { + void reportImport(NamespaceDirective directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithDartDirectiveGoFirstMessage( + directive, _importKeyword); + } + } + + void reportExport(NamespaceDirective directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithDartDirectiveGoFirstMessage( + directive, _exportKeyword); + } + } + + Iterable getNodesToLint( + Iterable directives) => + directives.skipWhile(_isDartDirective).where(_isDartDirective); + + getNodesToLint(_getImportDirectives(node)).forEach(reportImport); + + getNodesToLint(_getExportDirectives(node)).forEach(reportExport); + } + + void _checkDirectiveSectionOrderedAlphabetically( + Set lintedNodes, CompilationUnit node) { + var importDirectives = _getImportDirectives(node); + var exportDirectives = _getExportDirectives(node); + + var dartImports = importDirectives.where(_isDartDirective); + var dartExports = exportDirectives.where(_isDartDirective); + + var relativeImports = importDirectives.where(_isRelativeDirective); + var relativeExports = exportDirectives.where(_isRelativeDirective); + + _checkSectionInOrder(lintedNodes, dartImports); + _checkSectionInOrder(lintedNodes, dartExports); + + _checkSectionInOrder(lintedNodes, relativeImports); + _checkSectionInOrder(lintedNodes, relativeExports); + + // See: https://github.com/dart-lang/linter/issues/3395 + // (`DartProject` removal) + // The rub is that *all* projects are being treated as "not pub" + // packages. We'll want to be careful when fixing this since it + // will have ecosystem impact. + + // Not a pub package. Package directives should be sorted in one block. + var packageImports = importDirectives.where(_isPackageDirective); + var packageExports = exportDirectives.where(_isPackageDirective); + + _checkSectionInOrder(lintedNodes, packageImports); + _checkSectionInOrder(lintedNodes, packageExports); + + // The following is relying on projectName which is meant to come from + // a `DartProject` instance (but was not since the project was always null) + // else { + // var packageBox = _PackageBox(projectName); + // + // var thirdPartyPackageImports = + // importDirectives.where(packageBox._isNotOwnPackageDirective); + // var thirdPartyPackageExports = + // exportDirectives.where(packageBox._isNotOwnPackageDirective); + // + // var ownPackageImports = + // importDirectives.where(packageBox._isOwnPackageDirective); + // var ownPackageExports = + // exportDirectives.where(packageBox._isOwnPackageDirective); + // + // _checkSectionInOrder(lintedNodes, thirdPartyPackageImports); + // _checkSectionInOrder(lintedNodes, thirdPartyPackageExports); + // + // _checkSectionInOrder(lintedNodes, ownPackageImports); + // _checkSectionInOrder(lintedNodes, ownPackageExports); + // } + } + + void _checkExportDirectiveAfterImportDirective( + Set lintedNodes, CompilationUnit node) { + void reportDirective(Directive directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithExportDirectiveAfterImportDirectiveMessage( + directive); + } + } + + node.directives.reversed + .skipWhile(_isPartDirective) + .skipWhile(_isExportDirective) + .where(_isExportDirective) + .forEach(reportDirective); + } + + void _checkPackageDirectiveBeforeRelative( + Set lintedNodes, CompilationUnit node) { + void reportImport(NamespaceDirective directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithPackageDirectiveBeforeRelativeMessage( + directive, _importKeyword); + } + } + + void reportExport(NamespaceDirective directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithPackageDirectiveBeforeRelativeMessage( + directive, _exportKeyword); + } + } + + Iterable getNodesToLint( + Iterable directives) => + directives + .where(_isNotDartDirective) + .skipWhile(_isAbsoluteDirective) + .where(_isPackageDirective); + + getNodesToLint(_getImportDirectives(node)).forEach(reportImport); + + getNodesToLint(_getExportDirectives(node)).forEach(reportExport); + } + + void _checkSectionInOrder( + Set lintedNodes, Iterable nodes) { + void reportDirective(NamespaceDirective directive) { + if (lintedNodes.add(directive)) { + rule._reportLintWithDirectiveSectionOrderedAlphabeticallyMessage( + directive); + } + } + + NamespaceDirective? previousDirective; + for (var directive in nodes) { + if (previousDirective != null) { + var previousUri = previousDirective.uri.stringValue; + var directiveUri = directive.uri.stringValue; + if (previousUri != null && + directiveUri != null && + compareDirectives(previousUri, directiveUri) > 0) { + reportDirective(directive); + } + } + previousDirective = directive; + } + } + + Iterable _getExportDirectives(CompilationUnit node) => + node.directives.whereType(); + + Iterable _getImportDirectives(CompilationUnit node) => + node.directives.whereType(); +} diff --git a/pkg/linter/lib/src/rules/discarded_futures.dart b/pkg/linter/lib/src/rules/discarded_futures.dart new file mode 100644 index 00000000000..680105349fd --- /dev/null +++ b/pkg/linter/lib/src/rules/discarded_futures.dart @@ -0,0 +1,175 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't invoke asynchronous functions in non-async blocks."; + +const _details = r''' +Making asynchronous calls in non-`async` functions is usually the sign of a +programming error. In general these functions should be marked `async` and such +futures should likely be awaited (as enforced by `unawaited_futures`). + +**DON'T** invoke asynchronous functions in non-`async` blocks. + +**BAD:** +```dart +void recreateDir(String path) { + deleteDir(path); + createDir(path); +} + +Future deleteDir(String path) async {} + +Future createDir(String path) async {} +``` + +**GOOD:** +```dart +Future recreateDir(String path) async { + await deleteDir(path); + await createDir(path); +} + +Future deleteDir(String path) async {} + +Future createDir(String path) async {} +``` +'''; + +class DiscardedFutures extends LintRule { + static const LintCode code = LintCode('discarded_futures', + "Asynchronous function invoked in a non-'async' function.", + correctionMessage: + "Try converting the enclosing function to be 'async' and then " + "'await' the future."); + + DiscardedFutures() + : super( + name: 'discarded_futures', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + } +} + +class _InvocationVisitor extends RecursiveAstVisitor { + final LintRule rule; + _InvocationVisitor(this.rule); + + @override + void visitFunctionExpression(FunctionExpression node) { + if (node.body.isAsynchronous) return; + super.visitFunctionExpression(node); + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + if (node.staticInvokeType.isFuture) { + rule.reportLint(node.function); + } + super.visitFunctionExpressionInvocation(node); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.methodName.staticElement.isDartAsyncUnawaited) return; + if (node.staticInvokeType.isFuture) { + rule.reportLint(node.methodName); + } + super.visitMethodInvocation(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void check(FunctionBody body) { + if (body.isAsynchronous) return; + var visitor = _InvocationVisitor(rule); + body.accept(visitor); + } + + void checkVariables(VariableDeclarationList variables) { + if (variables.type?.type?.isFuture ?? false) return; + for (var variable in variables.variables) { + var initializer = variable.initializer; + if (initializer is FunctionExpression) { + check(initializer.body); + } + } + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + check(node.body); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + checkVariables(node.fields); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + if (node.returnType?.type.isFuture ?? false) return; + check(node.functionExpression.body); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.returnType?.type.isFuture ?? false) return; + check(node.body); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + checkVariables(node.variables); + } +} + +extension on DartType? { + bool get isFuture { + var self = this; + DartType? returnType; + if (self is FunctionType) { + returnType = self.returnType; + } + if (self is InterfaceType) { + returnType = self; + } + + return returnType != null && + (returnType.isDartAsyncFuture || returnType.isDartAsyncFutureOr); + } +} + +extension ElementExtension on Element? { + bool get isDartAsyncUnawaited { + var self = this; + return self is FunctionElement && + self.name == 'unawaited' && + self.library.isDartAsync; + } +} diff --git a/pkg/linter/lib/src/rules/do_not_use_environment.dart b/pkg/linter/lib/src/rules/do_not_use_environment.dart new file mode 100644 index 00000000000..954ecef91c1 --- /dev/null +++ b/pkg/linter/lib/src/rules/do_not_use_environment.dart @@ -0,0 +1,75 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Do not use environment declared variables.'; + +const _details = r''' +Using values derived from the environment at compile-time, creates +hidden global state and makes applications hard to understand and maintain. + +**DON'T** use `fromEnvironment` or `hasEnvironment` factory constructors. + +**BAD:** +```dart +const loggingLevel = + bool.hasEnvironment('logging') ? String.fromEnvironment('logging') : null; +``` +'''; + +class DoNotUseEnvironment extends LintRule { + static const LintCode code = + LintCode('do_not_use_environment', 'Invalid use of a declared variable.'); + + DoNotUseEnvironment() + : super( + name: 'do_not_use_environment', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var constructorNameNode = node.constructorName; + if (constructorNameNode.staticElement?.isFactory != true) { + return; + } + var staticType = node.staticType; + if (staticType == null) { + return; + } + var constructorName = constructorNameNode.name?.name; + if (constructorName == null) { + return; + } + + if (((staticType.isDartCoreBool || + staticType.isDartCoreInt || + staticType.isDartCoreString) && + constructorName == 'fromEnvironment') || + (staticType.isDartCoreBool && constructorName == 'hasEnvironment')) { + rule.reportLint(constructorNameNode); + } + } +} diff --git a/pkg/linter/lib/src/rules/empty_catches.dart b/pkg/linter/lib/src/rules/empty_catches.dart new file mode 100644 index 00000000000..2c22b571297 --- /dev/null +++ b/pkg/linter/lib/src/rules/empty_catches.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Avoid empty catch blocks.'; + +const _details = r''' +**AVOID** empty catch blocks. + +In general, empty catch blocks should be avoided. In cases where they are +intended, a comment should be provided to explain why exceptions are being +caught and suppressed. Alternatively, the exception identifier can be named with +underscores (e.g., `_`) to indicate that we intend to skip it. + +**BAD:** +```dart +try { + ... +} catch(exception) { } +``` + +**GOOD:** +```dart +try { + ... +} catch(e) { + // ignored, really. +} + +// Alternatively: +try { + ... +} catch(_) { } + +// Better still: +try { + ... +} catch(e) { + doSomething(e); +} +``` + +'''; + +class EmptyCatches extends LintRule { + static const LintCode code = LintCode('empty_catches', 'Empty catch block.', + correctionMessage: + 'Try adding statements to the block, adding a comment to the block, ' + "or removing the 'catch' clause."); + + EmptyCatches() + : super( + name: 'empty_catches', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCatchClause(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitCatchClause(CatchClause node) { + // Skip exceptions named with underscores. + var exceptionParameter = node.exceptionParameter; + if (exceptionParameter != null && + exceptionParameter.name.lexeme.isJustUnderscores) { + return; + } + + var body = node.body; + if (node.body.statements.isEmpty && + body.rightBracket.precedingComments == null) { + rule.reportLint(body); + } + } +} diff --git a/pkg/linter/lib/src/rules/empty_constructor_bodies.dart b/pkg/linter/lib/src/rules/empty_constructor_bodies.dart new file mode 100644 index 00000000000..f4e63ec3af6 --- /dev/null +++ b/pkg/linter/lib/src/rules/empty_constructor_bodies.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use `;` instead of `{}` for empty constructor bodies.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#do-use--instead-of--for-empty-constructor-bodies): + +**DO** use `;` instead of `{}` for empty constructor bodies. + +In Dart, a constructor with an empty body can be terminated with just a +semicolon. This is required for const constructors. For consistency and +brevity, other constructors should also do this. + +**BAD:** +```dart +class Point { + int x, y; + Point(this.x, this.y) {} +} +``` + +**GOOD:** +```dart +class Point { + int x, y; + Point(this.x, this.y); +} +``` + +'''; + +class EmptyConstructorBodies extends LintRule { + static const LintCode code = LintCode('empty_constructor_bodies', + "Empty constructor bodies should be written using a ';' rather than '{}'.", + correctionMessage: "Try replacing the constructor body with ';'."); + + EmptyConstructorBodies() + : super( + name: 'empty_constructor_bodies', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var body = node.body; + if (body is BlockFunctionBody) { + var block = body.block; + if (block.statements.isEmpty) { + if (block.endToken.precedingComments == null) { + rule.reportLint(block); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/empty_statements.dart b/pkg/linter/lib/src/rules/empty_statements.dart new file mode 100644 index 00000000000..5711da5fed5 --- /dev/null +++ b/pkg/linter/lib/src/rules/empty_statements.dart @@ -0,0 +1,91 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid empty statements.'; + +const _details = r''' +**AVOID** empty statements. + +Empty statements almost always indicate a bug. + +For example, + +**BAD:** +```dart +if (complicated.expression.foo()); + bar(); +``` + +Formatted with `dart format` the bug becomes obvious: + +```dart +if (complicated.expression.foo()) ; +bar(); + +``` + +Better to avoid the empty statement altogether. + +**GOOD:** +```dart +if (complicated.expression.foo()) + bar(); +``` + +'''; + +class EmptyStatements extends LintRule { + static const LintCode code = LintCode( + 'empty_statements', 'Unnecessary empty statement.', + correctionMessage: + 'Try removing the empty statement or restructuring the code.'); + + EmptyStatements() + : super( + name: 'empty_statements', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addEmptyStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool definesSemantics(EmptyStatement node) { + var parent = node.parent; + if (parent is! SwitchPatternCase) return false; + + var statements = parent.statements; + if (statements.last != node) return false; + + for (var statement in statements) { + if (statement is! EmptyStatement) return false; + } + + return true; + } + + @override + void visitEmptyStatement(EmptyStatement node) { + if (definesSemantics(node)) return; + rule.reportLint(node); + } +} diff --git a/pkg/linter/lib/src/rules/enable_null_safety.dart b/pkg/linter/lib/src/rules/enable_null_safety.dart new file mode 100644 index 00000000000..ae539a3345d --- /dev/null +++ b/pkg/linter/lib/src/rules/enable_null_safety.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = r'Do use sound null safety.'; + +const _details = r''' +**DO** use sound null safety, by not specifying a dart version lower than `2.12`. + +**BAD:** +```dart +// @dart=2.8 +a() { +} +``` + +**GOOD:** +```dart +b() { +} +``` +'''; + +class EnableNullSafety extends LintRule implements NodeLintRule { + static const LintCode code = LintCode( + 'enable_null_safety', 'Use sound null safety.', + correctionMessage: + "Try specifying a dart version greater than or equal to '2.12'."); + + EnableNullSafety() + : super( + name: 'enable_null_safety', + description: _desc, + details: _details, + state: State.removed(since: dart2_12), + group: Group.style); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/eol_at_end_of_file.dart b/pkg/linter/lib/src/rules/eol_at_end_of_file.dart new file mode 100644 index 00000000000..36180efc152 --- /dev/null +++ b/pkg/linter/lib/src/rules/eol_at_end_of_file.dart @@ -0,0 +1,73 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Put a single newline at end of file.'; + +const _details = r''' +**DO** put a single newline at the end of non-empty files. + +**BAD:** +```dart +a { +} +``` + +**GOOD:** +```dart +b { +} + <-- newline +``` +'''; + +class EolAtEndOfFile extends LintRule { + static const LintCode code = LintCode( + 'eol_at_end_of_file', 'Missing a newline at the end of the file.', + correctionMessage: 'Try adding a newline at the end of the file.'); + + EolAtEndOfFile() + : super( + name: 'eol_at_end_of_file', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCompilationUnit(CompilationUnit node) { + var content = context.currentUnit.content; + if (content.isNotEmpty && + (!content.endsWithNewline || content.endsWithMultipleNewlines)) { + rule.reportLintForOffset(content.trimRight().length, 1); + } + } +} + +extension on String { + static const newline = ['\n', '\r']; + static const multipleNewlines = ['\n\n', '\r\r', '\r\n\r\n']; + bool get endsWithMultipleNewlines => multipleNewlines.any(endsWith); + bool get endsWithNewline => newline.any(endsWith); +} diff --git a/pkg/linter/lib/src/rules/exhaustive_cases.dart b/pkg/linter/lib/src/rules/exhaustive_cases.dart new file mode 100644 index 00000000000..4a91aacdb4f --- /dev/null +++ b/pkg/linter/lib/src/rules/exhaustive_cases.dart @@ -0,0 +1,170 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Define case clauses for all constants in enum-like classes.'; + +const _details = r''' +Switching on instances of enum-like classes should be exhaustive. + +Enum-like classes are defined as concrete (non-abstract) classes that have: + * only private non-factory constructors + * two or more static const fields whose type is the enclosing class and + * no subclasses of the class in the defining library + +**DO** define case clauses for all constants in enum-like classes. + +**BAD:** +```dart +class EnumLike { + final int i; + const EnumLike._(this.i); + + static const e = EnumLike._(1); + static const f = EnumLike._(2); + static const g = EnumLike._(3); +} + +void bad(EnumLike e) { + // Missing case. + switch(e) { // LINT + case EnumLike.e : + print('e'); + break; + case EnumLike.f : + print('f'); + break; + } +} +``` + +**GOOD:** +```dart +class EnumLike { + final int i; + const EnumLike._(this.i); + + static const e = EnumLike._(1); + static const f = EnumLike._(2); + static const g = EnumLike._(3); +} + +void ok(EnumLike e) { + // All cases covered. + switch(e) { // OK + case EnumLike.e : + print('e'); + break; + case EnumLike.f : + print('f'); + break; + case EnumLike.g : + print('g'); + break; + } +} +``` +'''; + +class ExhaustiveCases extends LintRule { + static const LintCode code = LintCode( + 'exhaustive_cases', "Missing case clauses for some constants in '{0}'.", + correctionMessage: 'Try adding case clauses for the missing constants.'); + + ExhaustiveCases() + : super( + name: 'exhaustive_cases', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSwitchStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSwitchStatement(SwitchStatement statement) { + var expressionType = statement.expression.staticType; + if (expressionType is InterfaceType) { + var interfaceElement = expressionType.element; + // Handled in analyzer. + if (interfaceElement is! ClassElement) { + return; + } + var enumDescription = interfaceElement.asEnumLikeClass; + if (enumDescription == null) { + return; + } + + var enumConstants = enumDescription.enumConstants; + for (var member in statement.members) { + Expression? expression; + if (member is SwitchPatternCase) { + var pattern = member.guardedPattern.pattern.unParenthesized; + if (pattern is ConstantPattern) { + expression = pattern.expression.unParenthesized; + } + } else if (member is SwitchCase) { + expression = member.expression.unParenthesized; + } + if (expression is Identifier) { + var variable = expression.staticElement.variableElement; + if (variable is VariableElement) { + enumConstants.remove(variable.computeConstantValue()); + } + } else if (expression is PropertyAccess) { + var variable = expression.propertyName.staticElement.variableElement; + if (variable is VariableElement) { + enumConstants.remove(variable.computeConstantValue()); + } + } + if (member is SwitchDefault) { + return; + } + } + + for (var constant in enumConstants.keys) { + // Use the same offset as MISSING_ENUM_CONSTANT_IN_SWITCH. + var offset = statement.offset; + var end = statement.rightParenthesis.end; + var elements = enumConstants[constant]!; + var preferredElement = elements.firstWhere( + (element) => !element.hasDeprecated, + orElse: () => elements.first); + rule.reportLintForOffset( + offset, + end - offset, + arguments: [preferredElement.name], + ); + } + } + } +} + +extension on Element? { + Element? get variableElement { + var self = this; + if (self is PropertyAccessorElement) return self.variable; + return self; + } +} diff --git a/pkg/linter/lib/src/rules/file_names.dart b/pkg/linter/lib/src/rules/file_names.dart new file mode 100644 index 00000000000..551e3a58d8d --- /dev/null +++ b/pkg/linter/lib/src/rules/file_names.dart @@ -0,0 +1,84 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Name source files using `lowercase_with_underscores`.'; + +const _details = r''' +**DO** name source files using `lowercase_with_underscores`. + +Some file systems are not case-sensitive, so many projects require filenames to +be all lowercase. Using a separating character allows names to still be readable +in that form. Using underscores as the separator ensures that the name is still +a valid Dart identifier, which may be helpful if the language later supports +symbolic imports. + +**BAD:** + +* `SliderMenu.dart` +* `filesystem.dart` +* `file-system.dart` + +**GOOD:** + +* `slider_menu.dart` +* `file_system.dart` + +Files without a strict `.dart` extension are ignored. For example: + +**OK:** + +* `file-system.g.dart` +* `SliderMenu.css.dart` + +The lint `library_names` can be used to enforce the same kind of naming on the +library. + +'''; + +class FileNames extends LintRule { + static const LintCode code = LintCode( + 'file_names', "The file name '{0}' isn't a snake_case identifier.", + correctionMessage: + 'Try changing the name to follow the snake_case style.'); + + FileNames() + : super( + name: 'file_names', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitCompilationUnit(CompilationUnit node) { + var declaredElement = node.declaredElement; + if (declaredElement != null) { + var fileName = declaredElement.source.shortName; + if (!isValidDartFileName(fileName)) { + rule.reportLintForOffset(0, 0, arguments: [fileName]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/flutter_style_todos.dart b/pkg/linter/lib/src/rules/flutter_style_todos.dart new file mode 100644 index 00000000000..9d1c4ffb300 --- /dev/null +++ b/pkg/linter/lib/src/rules/flutter_style_todos.dart @@ -0,0 +1,87 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use Flutter TODO format: ' + '// TODO(username): message, https://URL-to-issue.'; + +const _details = r''' +**DO** use Flutter TODO format. + +From the [Flutter docs](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#comments): + +> TODOs should include the string TODO in all caps, followed by the GitHub username of the person with the best context about the problem referenced by the TODO in parenthesis. A TODO is not a commitment that the person referenced will fix the problem, it is intended to be the person with enough context to explain the problem. Thus, when you create a TODO, it is almost always your username that is given. + +**GOOD:** +```dart +// TODO(username): message. +// TODO(username): message, https://URL-to-issue. +``` + +'''; + +class FlutterStyleTodos extends LintRule { + static const LintCode code = LintCode( + 'flutter_style_todos', "To-do comment doesn't follow the Flutter style.", + correctionMessage: 'Try following the Flutter style for to-do comments.'); + + FlutterStyleTodos() + : super( + name: 'flutter_style_todos', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + static final _todoRegExp = RegExp(r'//+(.* )?TODO\b', caseSensitive: false); + + static final _todoExpectedRegExp = + RegExp(r'// TODO\([a-zA-Z0-9][-a-zA-Z0-9]*\): '); + + final LintRule rule; + + _Visitor(this.rule); + + void checkComments(Token token) { + Token? comment = token.precedingComments; + while (comment != null) { + _checkComment(comment); + comment = comment.next; + } + } + + @override + void visitCompilationUnit(CompilationUnit node) { + Token? token = node.beginToken; + while (token != null) { + checkComments(token); + if (token == token.next) break; + token = token.next; + } + } + + void _checkComment(Token node) { + var content = node.lexeme; + if (content.startsWith(_todoRegExp) && + !content.startsWith(_todoExpectedRegExp)) { + rule.reportLintForToken(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/hash_and_equals.dart b/pkg/linter/lib/src/rules/hash_and_equals.dart new file mode 100644 index 00000000000..a45deaa0c6e --- /dev/null +++ b/pkg/linter/lib/src/rules/hash_and_equals.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Always override `hashCode` if overriding `==`.'; + +const _details = r''' +**DO** override `hashCode` if overriding `==` and prefer overriding `==` if +overriding `hashCode`. + +Every object in Dart has a `hashCode`. Both the `==` operator and the +`hashCode` property of objects must be consistent in order for a common hash +map implementation to function properly. Thus, when overriding `==`, the +`hashCode` should also be overridden to maintain consistency. Similarly, if +`hashCode` is overridden, `==` should be also. + +**BAD:** +```dart +class Bad { + final int value; + Bad(this.value); + + @override + bool operator ==(Object other) => other is Bad && other.value == value; +} +``` + +**GOOD:** +```dart +class Better { + final int value; + Better(this.value); + + @override + bool operator ==(Object other) => + other is Better && + other.runtimeType == runtimeType && + other.value == value; + + @override + int get hashCode => value.hashCode; +} +``` +'''; + +class HashAndEquals extends LintRule { + static const LintCode code = LintCode( + 'hash_and_equals', "Missing a corresponding override of '{0}'.", + correctionMessage: "Try overriding '{0}' or removing '{1}'."); + + HashAndEquals() + : super( + name: 'hash_and_equals', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + _check(node.members); + } + + void _check(NodeList members) { + MethodDeclaration? eq; + ClassMember? hash; + for (var member in members) { + if (isEquals(member)) { + eq = member as MethodDeclaration; + } else if (isHashCode(member)) { + hash = member; + } + } + + if (eq != null && hash == null) { + rule.reportLintForToken(eq.name, arguments: ['hashCode', '==']); + } + if (hash != null && eq == null) { + if (hash is MethodDeclaration) { + rule.reportLintForToken(hash.name, arguments: ['==', 'hashCode']); + } else if (hash is FieldDeclaration) { + rule.reportLintForToken(getFieldName(hash, 'hashCode'), + arguments: ['==', 'hashCode']); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/implementation_imports.dart b/pkg/linter/lib/src/rules/implementation_imports.dart new file mode 100644 index 00000000000..8dbc11e504e --- /dev/null +++ b/pkg/linter/lib/src/rules/implementation_imports.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't import implementation files from another package."; + +const _details = r''' +From the the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files): + +**DON'T** import implementation files from another package. + +The libraries inside `lib` are publicly visible: other packages are free to +import them. But much of a package's code is internal implementation libraries +that should only be imported and used by the package itself. Those go inside a +subdirectory of `lib` called `src`. You can create subdirectories in there if +it helps you organize things. + +You are free to import libraries that live in `lib/src` from within other Dart +code in the same package (like other libraries in `lib`, scripts in `bin`, +and tests) but you should never import from another package's `lib/src` +directory. Those files are not part of the package's public API, and they +might change in ways that could break your code. + +**BAD:** +```dart +// In 'road_runner' +import 'package:acme/src/internals.dart'; +``` + +'''; + +bool isImplementation(Uri? uri) { + var segments = uri?.pathSegments ?? const []; + if (segments.length > 2) { + if (segments[1] == 'src') { + return true; + } + } + return false; +} + +bool isPackage(Uri? uri) => uri?.scheme == 'package'; + +bool samePackage(Uri? uri1, Uri? uri2) { + if (uri1 == null || uri2 == null) { + return false; + } + var segments1 = uri1.pathSegments; + var segments2 = uri2.pathSegments; + if (segments1.isEmpty || segments2.isEmpty) { + return false; + } + return segments1.first == segments2.first; +} + +class ImplementationImports extends LintRule { + static const LintCode code = LintCode('implementation_imports', + "Import of a library in the 'lib/src' directory of another package.", + correctionMessage: + 'Try importing a public library that exports this library, or ' + 'removing the import.'); + + ImplementationImports() + : super( + name: 'implementation_imports', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitImportDirective(ImportDirective node) { + var importUri = node.element?.importedLibrary?.source.uri; + var sourceUri = node.element?.source.uri; + + // Test for 'package:*/src/'. + if (!isImplementation(importUri)) { + return; + } + + // If the source URI is not a `package` URI bail out. + if (!isPackage(sourceUri)) { + return; + } + + if (!samePackage(importUri, sourceUri)) { + rule.reportLint(node.uri); + } + } +} diff --git a/pkg/linter/lib/src/rules/implicit_call_tearoffs.dart b/pkg/linter/lib/src/rules/implicit_call_tearoffs.dart new file mode 100644 index 00000000000..61b90105951 --- /dev/null +++ b/pkg/linter/lib/src/rules/implicit_call_tearoffs.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Explicitly tear-off `call` methods when using an object as a Function.'; + +const _details = r''' +**DO** +Explicitly tear off `.call` methods from objects when assigning to a Function +type. There is less magic with an explicit tear off. Future language versions +may remove the implicit call tear off. + +**BAD:** +```dart +class Callable { + void call() {} +} +void callIt(void Function() f) { + f(); +} + +callIt(Callable()); +``` + +**GOOD:** +```dart +class Callable { + void call() {} +} +void callIt(void Function() f) { + f(); +} + +callIt(Callable().call); +``` + +'''; + +class ImplicitCallTearoffs extends LintRule { + static const LintCode code = LintCode( + 'implicit_call_tearoffs', "Implicit tear-off of the 'call' method.", + correctionMessage: "Try explicitly tearing off the 'call' method."); + + ImplicitCallTearoffs() + : super( + name: 'implicit_call_tearoffs', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addImplicitCallReference(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitImplicitCallReference(ImplicitCallReference node) { + rule.reportLint(node); + } +} diff --git a/pkg/linter/lib/src/rules/implicit_reopen.dart b/pkg/linter/lib/src/rules/implicit_reopen.dart new file mode 100644 index 00000000000..911b9c8c6ad --- /dev/null +++ b/pkg/linter/lib/src/rules/implicit_reopen.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't implicitly reopen classes."; + +/// todo(pq): link out to (upcoming) dart.dev docs. +/// https://github.com/dart-lang/site-www/issues/4497 +const _details = r''' +Using an `interface`, `base`, `final`, or `sealed` modifier on a class, +or a `base` modifier on a mixin, +authors can control whether classes and mixins allow being implemented, +extended, and/or mixed in from outside of the library where they're defined. +In some cases, it's possible for an author to inadvertently relax these controls +and implicitly "reopen" a class. (A similar reopening cannot occur with a mixin.) + +This lint guards against unintentionally reopening a class by requiring such +cases to be made explicit with the +[`@reopen`](https://pub.dev/documentation/meta/latest/meta/reopen-constant.html) +annotation in `package:meta`. + +**BAD:** +```dart +interface class I {} + +class C extends I {} // LINT +``` + +**GOOD:** +```dart +interface class I {} + +final class C extends I {} +``` + +```dart +interface class I {} + +final class C extends I {} +``` + +```dart +import 'package:meta/meta.dart'; + +interface class I {} + +@reopen +class C extends I {} +``` +'''; + +class ImplicitReopen extends LintRule { + static const LintCode code = LintCode('implicit_reopen', + "The {0} '{1}' reopens '{2}' because it is not marked '{3}'", + correctionMessage: + "Try marking '{1}' '{3}' or annotating it with '@reopen'"); + + ImplicitReopen() + : super( + name: 'implicit_reopen', + description: _desc, + details: _details, + state: State.experimental(), + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addClassTypeAlias(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkElement(InterfaceElement? element, NamedCompilationUnitMember node, + {required String type}) { + if (element is! ClassElement) return; + if (element.hasReopen) return; + if (element.isSealed) return; + if (element.isMixinClass) return; + + var library = element.library; + var supertype = element.supertype?.element; + if (supertype is! ClassElement) return; + if (supertype.library != library) return; + + if (element.isBase) { + if (supertype.isFinal) { + reportLint(node, + target: element, other: supertype, reason: 'final', type: type); + return; + } else if (supertype.isInterface) { + reportLint(node, + target: element, other: supertype, reason: 'interface', type: type); + return; + } + } else if (element.hasNoModifiers) { + if (supertype.isInterface) { + reportLint(node, + target: element, other: supertype, reason: 'interface', type: type); + return; + } + } + } + + void reportLint( + NamedCompilationUnitMember member, { + required String type, + required InterfaceElement target, + required InterfaceElement other, + required String reason, + }) { + rule.reportLintForToken(member.name, + arguments: [type, target.name, other.name, reason]); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + checkElement(node.declaredElement, node, type: 'class'); + } + + @override + visitClassTypeAlias(ClassTypeAlias node) { + checkElement(node.declaredElement, node, type: 'class'); + } +} + +extension on ClassElement { + bool get hasNoModifiers => !isInterface && !isBase && !isSealed && !isFinal; +} diff --git a/pkg/linter/lib/src/rules/invalid_case_patterns.dart b/pkg/linter/lib/src/rules/invalid_case_patterns.dart new file mode 100644 index 00000000000..c357f820f1c --- /dev/null +++ b/pkg/linter/lib/src/rules/invalid_case_patterns.dart @@ -0,0 +1,326 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use case expressions that are valid in Dart 3.0.'; + +const _details = r''' +Some case expressions that are valid in Dart 2.19 and below will become an error +or have changed semantics when a library is upgraded to 3.0. This lint flags +those expressions in order to ease migration to Dart 3.0. + +Some valid switch cases in 2.19 will become compile errors in Dart 3.0: + +* Set literals +* Parenthesized expressions +* Calls to `identical()`. +* Unary operator expressions `!`, `-`, or `~` (except for `-` before an integer + literal, which is a valid pattern and is fine) +* Binary operator expressions `!=`, `==`, `&`, `|`, `^`, `~/`, `>>`, `>>>`, + `<<`, `+`, `-`, `*`, `/`, `%`, `<`, `<=`, `>`, `>=`, `??`. +* Conditional operator `?:` +* `.length` calls on strings +* `is` and `is!` expressions + +Examples of all of them: + +```dart +switch (obj) { + case {1}: // Set literal. + case (1): // Parenthesized expression. + case identical(1, 2): // `identical()` call. + case -pi: // Unary operator. + case 1 + 2: // Binary operator. + case true ? 1 : 2: // Conditional operator. + case 'hi'.length: // .length call. + case i is int: // is expression. +} +``` + +Some valid switch cases in 2.19 are also syntactically valid patterns, but the +pattern matching behavior may be different from the current constant equality +behavior. They are: + +**List and map literals.** A list or map literal can appear as a constant in a +case: + +```dart +switch (obj) { + case [1, 2]: ... + case {'k': 'v'}: ... +} +``` + +Currently, the case will only match if the incoming value has the same identity +as the constant. So: + +```dart +test(List list) { + switch (list) { + case [1, 2]: print('Matched'); break; + default: print('Did not match'); break; + } +} + +main() { + test(const [1, 2]); // Prints "Matched". + test([1, 2]); // Prints "Did not match". +} +``` + +With patterns, a list or map literal becomes a list or map pattern. The pattern +destructures the incoming object and matches if the subpatterns all match. In +other words, list and map pattern match using something more like deep equality. + +With Dart 3.0, the above program prints "Matched" twice. + +**Constant constructor calls.** Similar to collections, you can construct a +constant instance of a class in a case: + +```dart +class Point { + final int x; + final int y; + const Point({this.x, this.y}); +} + +test(Point p) { + switch (p) { + case Point(x: 1, y: 2): print('Matched'); break; + default: print('Did not match'); break; + } +} + +main() { + test(const Point(1, 2)); // Prints "Matched". + test(Point(1, 2)); // Prints "Did not match". +} +``` + +Again, like collections, the case currently only matches if the incoming value +has the same identity. With patterns, the `Point(...)` syntax becomes an object +pattern that destructures the incoming point, calls the `x` and `y` getters on +it and then matches the results of those against the corresponding subpatterns. + +In this example, it will print "Matched" twice. + +Note that object patterns only support named fields. So any constant constructor +in a case today that has positional arguments will become a compile-time error +when parsed as a pattern. A constant constructor call with no arguments is a +valid object pattern and only does a type test: + +```dart +class Thing { + const Thing(); +} + +test(Thing t) { + switch (t) { + case Thing(): print('Matched'); break; + default: print('Did not match'); break; + } +} + +main() { + test(const Thing()); // Prints "Matched". + test(Thing()); // Prints "Did not match". +} +``` + +When interpreted as a pattern, this prints "Matched" twice. + +**Wildcards.** Today, you can have a constant named `_`: + +```dart +test(int n) { + const _ = 3; + switch (n) { + case _: print('Matched'); break; + default: print('Did not match'); break; + } +} + +main() { + test(3); // Prints "Matched". + test(5); // Prints "Did not match". +} +``` + +With patterns, the identifier `_` is treated as a pattern that matches all +values, so this prints "Matched" twice. + +**Logic operators.** The logic operators `&&` and `||` are valid constant +expressions and also valid patterns. As a constant expression, they simply +evaluate the expression to a boolean and match if the incoming value is equal to +that boolean value. So: + +```dart +test(bool b) { + switch (b) { + case true && false: print('Matched'); break; + default: print('Did not match'); break; + } +} + +main() { + test(false); // Prints "Matched". + test(true); // Prints "Did not match". +} +``` + +With Dart 3.0, these become patterns. The above example prints "Did not match" +twice because no boolean value can be both true and false. + +Many of invalid cases can be mechanically changed to something that is valid +both in Dart today and valid and means the same in Dart 3.0. + +**Parenthesized expressions:** Provided the inner expression is one that's not +broken in Dart 3.0, just discard the parentheses. + +**List literals, map literals, set literals, and constant constructor calls:** +Put `const` before the literal or call. This turns it into a constant pattern +which preserves the current behavior: + +**BAD:** + +```dart +case [1, 2]: +case {'k': 'v'}: +case {1, 2}: +case Point(1, 2): +``` + +**GOOD:** + +```dart +case const [1, 2]: +case const {'k': 'v'}: +case const {1, 2}: +case const Point(1, 2): +``` + +* **Wildcards:** Rename the constant from `_` to something else. Since the name +is private, this can be done locally in the library without affecting other +code. + +* **Everything else:** For any other invalid expression, you have to hoist the +expression out into a new named constant. For example, if you have code like +this: + + +**BAD:** + +```dart +switch (n) { + case 1 + 2: ... +} +``` + +It can be fixed by changing it to: + +**GOOD:** + + ```dart +const three = 1 + 2; + +switch (n) { + case three: ... +} +``` +'''; + +// todo(pq): add a dart.dev doc link when a doc is final (https://github.com/dart-lang/linter/issues/4055) + +class InvalidCasePatterns extends LintRule { + static const LintCode code = LintCode('invalid_case_patterns', + "This expression is not valid in a 'case' clause in Dart 3.0.", + correctionMessage: 'Try refactoring the expression to be valid in 3.0.'); + + InvalidCasePatterns() + : super( + name: 'invalid_case_patterns', + description: _desc, + details: _details, + state: State.experimental(), + group: Group.errors); + + /// todo(pq): update to add specific messages w/ specific corrections + /// https://github.com/dart-lang/linter/issues/4172 + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSwitchCase(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + visitSwitchCase(SwitchCase node) { + var featureSet = node.thisOrAncestorOfType()?.featureSet; + if (featureSet != null && featureSet.isEnabled(Feature.patterns)) { + // This lint rule is only meant for code which does not have 'patterns' + // enabled. + return; + } + var expression = node.expression.unParenthesized; + if (expression is SetOrMapLiteral) { + if (expression.constKeyword == null) { + rule.reportLint(expression); + } + } else if (expression is ListLiteral) { + if (expression.constKeyword == null) { + rule.reportLint(expression); + } + } else if (expression is MethodInvocation) { + if (expression.methodName.isDartCoreIdentifier(named: 'identical')) { + rule.reportLint(expression); + } + } else if (expression is PrefixExpression) { + if (expression.operand is! IntegerLiteral) { + rule.reportLint(expression); + } + } else if (expression is BinaryExpression) { + rule.reportLint(expression); + } else if (expression is ConditionalExpression) { + rule.reportLint(expression); + } else if (expression is PropertyAccess) { + if (expression.propertyName.isDartCoreIdentifier(named: 'length')) { + rule.reportLint(expression); + } + } else if (expression is IsExpression) { + rule.reportLint(expression); + } else if (expression is InstanceCreationExpression) { + if (expression.isConst && expression.keyword?.type != Keyword.CONST) { + rule.reportLint(expression); + } + } else if (expression is SimpleIdentifier) { + var token = expression.token; + if (token is StringToken && token.lexeme == '_') { + rule.reportLint(expression); + } + } + } +} + +extension on SimpleIdentifier { + bool isDartCoreIdentifier({required String named}) { + if (name != named) return false; + var library = staticElement?.library; + return library != null && library.isDartCore; + } +} diff --git a/pkg/linter/lib/src/rules/invariant_booleans.dart b/pkg/linter/lib/src/rules/invariant_booleans.dart new file mode 100644 index 00000000000..3606490a435 --- /dev/null +++ b/pkg/linter/lib/src/rules/invariant_booleans.dart @@ -0,0 +1,111 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = + r'Conditions should not unconditionally evaluate to `true` or to `false`.'; + +const _details = r''' +**DON'T** test for conditions that can be inferred at compile time or test the +same condition twice. + +Conditional statements using a condition which cannot be anything but `false` +have the effect of making blocks of code non-functional. If the condition +cannot evaluate to anything but `true`, the conditional statement is completely +redundant, and makes the code less readable. +It is quite likely that the code does not match the programmer's intent. +Either the condition should be removed or it should be updated so that it does +not always evaluate to `true` or `false` and does not perform redundant tests. +This rule will hint to the test conflicting with the linted one. + +**BAD:** +```dart +// foo can't be both equal and not equal to bar in the same expression +if(foo == bar && something && foo != bar) {...} +``` + +**BAD:** +```dart +void compute(int foo) { + if (foo == 4) { + doSomething(); + // we know foo is equal to 4 at this point, so the next condition is always false + if (foo > 4) {...} + ... + } + ... +} +``` + +**BAD:** +```dart +void compute(bool foo) { + if (foo) { + return; + } + doSomething(); + // foo is always false here + if (foo){...} + ... +} +``` + +**GOOD:** +```dart +void nestedOK() { + if (foo == bar) { + foo = baz; + if (foo != bar) {...} + } +} +``` + +**GOOD:** +```dart +void nestedOk2() { + if (foo == bar) { + return; + } + + foo = baz; + if (foo == bar) {...} // OK +} +``` + +**GOOD:** +```dart +void nestedOk5() { + if (foo != null) { + if (bar != null) { + return; + } + } + + if (bar != null) {...} // OK +} +``` + +'''; + +class InvariantBooleans extends LintRule { + static const LintCode code = LintCode( + 'invariant_booleans', 'Condition always evaluates to the same value.', + correctionMessage: + 'Try removing the condition or changing it to not produce the same ' + 'result.'); + + InvariantBooleans() + : super( + name: 'invariant_booleans', + description: _desc, + details: _details, + // todo(pq): remove `since` once analyzer 5.5.0 is published and can + // be unspecified. + state: State.removed(since: dart3), + group: Group.errors); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/iterable_contains_unrelated_type.dart b/pkg/linter/lib/src/rules/iterable_contains_unrelated_type.dart new file mode 100644 index 00000000000..2ccd571957c --- /dev/null +++ b/pkg/linter/lib/src/rules/iterable_contains_unrelated_type.dart @@ -0,0 +1,160 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; +import '../util/unrelated_types_visitor.dart'; + +const _desc = r'Invocation of Iterable.contains with references of unrelated' + r' types.'; + +const _details = r''' +**DON'T** invoke `contains` on `Iterable` with an instance of different type +than the parameter type. + +Doing this will invoke `==` on its elements and most likely will return `false`. + +**BAD:** +```dart +void someFunction() { + var list = []; + if (list.contains('1')) print('someFunction'); // LINT +} +``` + +**BAD:** +```dart +void someFunction3() { + List list = []; + if (list.contains('1')) print('someFunction3'); // LINT +} +``` + +**BAD:** +```dart +void someFunction8() { + List list = []; + DerivedClass3 instance; + if (list.contains(instance)) print('someFunction8'); // LINT +} +``` + +**BAD:** +```dart +abstract class SomeIterable implements Iterable {} + +abstract class MyClass implements SomeIterable { + bool badMethod(String thing) => this.contains(thing); // LINT +} +``` + +**GOOD:** +```dart +void someFunction10() { + var list = []; + if (list.contains(1)) print('someFunction10'); // OK +} +``` + +**GOOD:** +```dart +void someFunction1() { + var list = []; + if (list.contains(1)) print('someFunction1'); // OK +} +``` + +**GOOD:** +```dart +void someFunction4() { + List list = []; + if (list.contains(1)) print('someFunction4'); // OK +} +``` + +**GOOD:** +```dart +void someFunction5() { + List list = []; + DerivedClass1 instance; + if (list.contains(instance)) print('someFunction5'); // OK +} + +abstract class ClassBase {} + +class DerivedClass1 extends ClassBase {} +``` + +**GOOD:** +```dart +void someFunction6() { + List list = []; + DerivedClass2 instance; + if (list.contains(instance)) print('someFunction6'); // OK +} + +abstract class ClassBase {} + +abstract class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} +``` + +**GOOD:** +```dart +void someFunction7() { + List list = []; + DerivedClass3 instance; + if (list.contains(instance)) print('someFunction7'); // OK +} + +abstract class ClassBase {} + +abstract class Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} +``` + +**DEPRECATED:** This rule is deprecated in favor of +`collection_methods_unrelated_type`. +The rule will be removed in a future Dart release. + +'''; + +class IterableContainsUnrelatedType extends LintRule { + static const LintCode code = LintCode('iterable_contains_unrelated_type', + "The argument type '{0}' isn't related to '{1}'."); + + IterableContainsUnrelatedType() + : super( + name: 'iterable_contains_unrelated_type', + description: _desc, + details: _details, + group: Group.errors, + state: + State.deprecated(replacedBy: 'collection_methods_unrelated_type'), + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context.typeSystem, context.typeProvider); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends UnrelatedTypesProcessors { + _Visitor(super.rule, super.typeSystem, super.typeProvider); + + @override + List get methods => [ + MethodDefinitionForElement( + typeProvider.iterableElement, + 'contains', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ) + ]; +} diff --git a/pkg/linter/lib/src/rules/join_return_with_assignment.dart b/pkg/linter/lib/src/rules/join_return_with_assignment.dart new file mode 100644 index 00000000000..084f51c17d3 --- /dev/null +++ b/pkg/linter/lib/src/rules/join_return_with_assignment.dart @@ -0,0 +1,119 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/dart_type_utilities.dart' as type_utils; + +const _desc = r'Join return statement with assignment when possible.'; + +const _details = r''' +**DO** join return statement with assignment when possible. + +**BAD:** +```dart +class A { + B _lazyInstance; + static B get instance { + _lazyInstance ??= B(); // LINT + return _lazyInstance; + } +} +``` + +**GOOD:** +```dart +class A { + B _lazyInstance; + static B get instance => _lazyInstance ??= B(); +} +``` + +'''; + +Expression? _getExpressionFromAssignmentStatement(Statement node) { + if (node is ExpressionStatement) { + var expression = node.expression.unParenthesized; + if (expression is AssignmentExpression) { + return expression.leftHandSide; + } else if (expression is PostfixExpression) { + return expression.operand; + } else if (expression is PrefixExpression) { + return expression.operand; + } + } + return null; +} + +Expression? _getExpressionFromReturnStatement(Statement node) => + node is ReturnStatement ? node.expression : null; + +class JoinReturnWithAssignment extends LintRule { + static const LintCode code = LintCode('join_return_with_assignment', + "Assignment could be inlined in 'return' statement.", + correctionMessage: + "Try inlining the assigned value in the 'return' statement."); + + JoinReturnWithAssignment() + : super( + name: 'join_return_with_assignment', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBlock(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBlock(Block node) { + var statements = node.statements; + var length = statements.length; + if (length < 2) { + return; + } + var lastExpression = _getExpressionFromReturnStatement(statements.last); + + // In this case, the last statement was not a return statement with a + // simple target. + if (lastExpression == null) { + return; + } + + var secondLastStatement = statements[length - 2]; + var secondLastExpression = + _getExpressionFromAssignmentStatement(secondLastStatement); + // Return if the second-to-last statement was not an assignment. + if (secondLastExpression == null) { + return; + } + + Expression? thirdLastExpression; + if (length >= 3) { + var thirdLastStatement = statements[length - 3]; + thirdLastExpression = + _getExpressionFromAssignmentStatement(thirdLastStatement); + } + if (!type_utils.canonicalElementsFromIdentifiersAreEqual( + secondLastExpression, thirdLastExpression) && + type_utils.canonicalElementsFromIdentifiersAreEqual( + lastExpression, secondLastExpression)) { + rule.reportLint(secondLastStatement); + } + } +} diff --git a/pkg/linter/lib/src/rules/leading_newlines_in_multiline_strings.dart b/pkg/linter/lib/src/rules/leading_newlines_in_multiline_strings.dart new file mode 100644 index 00000000000..0880a4ba6e7 --- /dev/null +++ b/pkg/linter/lib/src/rules/leading_newlines_in_multiline_strings.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Start multiline strings with a newline.'; + +const _details = r""" +Multiline strings are easier to read when they start with a newline (a newline +starting a multiline string is ignored). + +**BAD:** +```dart +var s1 = '''{ + "a": 1, + "b": 2 +}'''; +``` + +**GOOD:** +```dart +var s1 = ''' +{ + "a": 1, + "b": 2 +}'''; + +var s2 = '''This one-liner multiline string is ok. It usually allows to escape both ' and " in the string.'''; +``` + +"""; + +class LeadingNewlinesInMultilineStrings extends LintRule { + static const LintCode code = LintCode('leading_newlines_in_multiline_strings', + 'Missing a newline at the beginning of a multiline string.', + correctionMessage: + 'Try adding a newline at the beginning of the string.'); + + LeadingNewlinesInMultilineStrings() + : super( + name: 'leading_newlines_in_multiline_strings', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + registry.addSimpleStringLiteral(this, visitor); + registry.addStringInterpolation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + LineInfo? lineInfo; + + _Visitor(this.rule); + + @override + void visitCompilationUnit(CompilationUnit node) { + lineInfo = node.lineInfo; + } + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + _visitSingleStringLiteral(node, node.literal.lexeme); + } + + @override + void visitStringInterpolation(StringInterpolation node) { + _visitSingleStringLiteral( + node, (node.elements.first as InterpolationString).contents.lexeme); + } + + void _visitSingleStringLiteral(SingleStringLiteral node, String lexeme) { + var nodeLineInfo = lineInfo; + if (nodeLineInfo == null) { + return; + } + if (node.isMultiline && + nodeLineInfo.getLocation(node.offset).lineNumber != + nodeLineInfo.getLocation(node.end).lineNumber) { + bool startWithNewLine(int index) => + lexeme.startsWith('\n', index) || lexeme.startsWith('\r', index); + if (!startWithNewLine(node.isRaw ? 4 : 3)) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/library_annotations.dart b/pkg/linter/lib/src/rules/library_annotations.dart new file mode 100644 index 00000000000..54521b6777c --- /dev/null +++ b/pkg/linter/lib/src/rules/library_annotations.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/dart/element/extensions.dart'; +import 'package:meta/meta_meta.dart'; + +import '../analyzer.dart'; + +const _desc = r'Attach library annotations to library directives.'; + +const _details = r''' +Attach library annotations to library directives, rather than +some other library-level element. + +**BAD:** +```dart +@TestOn('browser') + +import 'package:test/test.dart'; + +void main() {} +``` + +**GOOD:** +```dart +@TestOn('browser') +library; + +import 'package:test/test.dart'; + +void main() {} +``` + +**NOTE:** An unnamed library, like `library;` above, is only supported in Dart +2.19 and later. Code which might run in earlier versions of Dart will need to +provide a name in the `library` directive. +'''; + +class LibraryAnnotations extends LintRule { + static const LintCode code = LintCode('library_annotations', + 'This annotation must be attached to a library directive.', + correctionMessage: + 'Try attaching library annotations to library directives.'); + + LibraryAnnotations() + : super( + name: 'library_annotations', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LibraryAnnotations rule; + + _Visitor(this.rule); + + Directive? firstDirective; + + @override + void visitCompilationUnit(CompilationUnit node) { + if (node.directives.isNotEmpty) { + firstDirective = node.directives.first; + } + for (var directive in node.directives) { + if (directive is PartDirective) { + return; + } + if (directive is! LibraryDirective) { + _check(directive); + } + } + + node.declarations.forEach(_check); + } + + void _check(AnnotatedNode node) { + for (var annotation in node.metadata) { + var elementAnnotation = annotation.elementAnnotation; + if (elementAnnotation == null) { + return; + } + + if (elementAnnotation.targetKinds.length == 1 && + elementAnnotation.targetKinds.contains(TargetKind.library) && + firstDirective == node) { + rule.reportLint(annotation); + } else if (elementAnnotation.isPragmaLateTrust) { + rule.reportLint(annotation); + } + } + } +} + +extension on ElementAnnotation { + /// Whether this is an annotation of the form `@pragma('dart2js:late:trust')`. + bool get isPragmaLateTrust { + if (_isConstructor(libraryName: 'dart.core', className: 'pragma')) { + var value = computeConstantValue(); + var nameValue = value?.getField('name'); + return nameValue?.toStringValue() == 'dart2js:late:trust'; + } + return false; + } + + // Copied from package:analyzer/src/dart/element/element.dart + bool _isConstructor({ + required String libraryName, + required String className, + }) { + var element = this.element; + return element is ConstructorElement && + element.enclosingElement.name == className && + element.library.name == libraryName; + } +} diff --git a/pkg/linter/lib/src/rules/library_names.dart b/pkg/linter/lib/src/rules/library_names.dart new file mode 100644 index 00000000000..db987f8844f --- /dev/null +++ b/pkg/linter/lib/src/rules/library_names.dart @@ -0,0 +1,73 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../utils.dart'; + +const _desc = r'Name libraries using `lowercase_with_underscores`.'; + +const _details = r''' +**DO** name libraries using `lowercase_with_underscores`. + +Some file systems are not case-sensitive, so many projects require filenames to +be all lowercase. Using a separating character allows names to still be readable +in that form. Using underscores as the separator ensures that the name is still +a valid Dart identifier, which may be helpful if the language later supports +symbolic imports. + +**BAD:** +```dart +library peg-parser; +``` + +**GOOD:** +```dart +library peg_parser; +``` + +The lint `file_names` can be used to enforce the same kind of naming on the +file. + +'''; + +class LibraryNames extends LintRule { + static const LintCode code = LintCode( + 'library_names', "The library name '{0}' isn't a snake_case identifier.", + correctionMessage: + 'Try changing the name to follow the snake_case style.'); + + LibraryNames() + : super( + name: 'library_names', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addLibraryDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitLibraryDirective(LibraryDirective node) { + var name = node.name2; + if (name != null && !isLowerCaseUnderScoreWithDots(name.toString())) { + rule.reportLint(name, arguments: [name.toString()]); + } + } +} diff --git a/pkg/linter/lib/src/rules/library_prefixes.dart b/pkg/linter/lib/src/rules/library_prefixes.dart new file mode 100644 index 00000000000..e9dded73e2f --- /dev/null +++ b/pkg/linter/lib/src/rules/library_prefixes.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../utils.dart'; + +const _desc = + r'Use `lowercase_with_underscores` when specifying a library prefix.'; + +const _details = r''' +**DO** use `lowercase_with_underscores` when specifying a library prefix. + +**BAD:** +```dart +import 'dart:math' as Math; +import 'dart:json' as JSON; +import 'package:js/js.dart' as JS; +import 'package:javascript_utils/javascript_utils.dart' as jsUtils; +``` + +**GOOD:** +```dart +import 'dart:math' as math; +import 'dart:json' as json; +import 'package:js/js.dart' as js; +import 'package:javascript_utils/javascript_utils.dart' as js_utils; +``` + +'''; + +class LibraryPrefixes extends LintRule { + static const LintCode code = LintCode( + 'library_prefixes', "The prefix '{0}' isn't a snake_case identifier.", + correctionMessage: + 'Try changing the prefix to follow the snake_case style.'); + + LibraryPrefixes() + : super( + name: 'library_prefixes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitImportDirective(ImportDirective node) { + var prefix = node.prefix; + if (prefix != null && !isValidLibraryPrefix(prefix.toString())) { + rule.reportLint(prefix, arguments: [prefix.toString()]); + } + } +} diff --git a/pkg/linter/lib/src/rules/library_private_types_in_public_api.dart b/pkg/linter/lib/src/rules/library_private_types_in_public_api.dart new file mode 100644 index 00000000000..26043d78e22 --- /dev/null +++ b/pkg/linter/lib/src/rules/library_private_types_in_public_api.dart @@ -0,0 +1,333 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid using private types in public APIs.'; + +const _details = r''' +**AVOID** using library private types in public APIs. + +For the purposes of this lint, a public API is considered to be any top-level or +member declaration unless the declaration is library private or contained in a +declaration that's library private. The following uses of types are checked: + +- the return type of a function or method, +- the type of any parameter of a function or method, +- the bound of a type parameter to any function, method, class, mixin, + extension's extended type, or type alias, +- the type of any top level variable or field, +- any type used in the declaration of a type alias (for example + `typedef F = _Private Function();`), or +- any type used in the `on` clause of an extension or a mixin + +**BAD:** +```dart +f(_Private p) { ... } +class _Private {} +``` + +**GOOD:** +```dart +f(String s) { ... } +``` + +'''; + +class LibraryPrivateTypesInPublicApi extends LintRule { + static const LintCode code = LintCode('library_private_types_in_public_api', + 'Invalid use of a private type in a public API.', + correctionMessage: + 'Try making the private type public, or making the API that uses the ' + 'private type also be private.'); + + LibraryPrivateTypesInPublicApi() + : super( + name: 'library_private_types_in_public_api', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = Visitor(this); + registry.addCompilationUnit(this, visitor); + } +} + +class Validator extends SimpleAstVisitor { + LintRule rule; + + Validator(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.typeParameters?.accept(this); + node.members.accept(this); + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.superclass.accept(this); + node.typeParameters?.accept(this); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var name = node.name; + if (name != null && Identifier.isPrivateName(name.lexeme)) { + return; + } + + var parent = node.parent; + + // Enum constructors are effectively private so don't visit their params. + if (parent is EnumDeclaration) return; + + // Select modified class types are also effectively private. + if (parent != null && parent.isEffectivelyPrivate) return; + + node.parameters.accept(this); + } + + @override + void visitDefaultFormalParameter(DefaultFormalParameter node) { + node.parameter.accept(this); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.typeParameters?.accept(this); + node.members.accept(this); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + var name = node.name; + if (name == null || Identifier.isPrivateName(name.lexeme)) { + return; + } + node.extendedType.accept(this); + node.typeParameters?.accept(this); + node.members.accept(this); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) return; + node.typeParameters?.accept(this); + var representation = node.representation; + if (!Identifier.isPrivateName(representation.fieldName.lexeme)) { + representation.fieldType.accept(this); + } + node.members.accept(this); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isInvalidExtensionTypeField) return; + if (node.fields.variables + .any((field) => !Identifier.isPrivateName(field.name.lexeme))) { + node.fields.type?.accept(this); + } + } + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + if (node.isNamed && Identifier.isPrivateName(node.name.lexeme)) { + return; + } + // Check for a declared type. + var type = node.type; + if (type != null) { + type.accept(this); + return; + } + + // Check implicit type. + var element = node.declaredElement; + if (element is FieldFormalParameterElement) { + var type = element.type; + if (type is InterfaceType && isPrivateName(type.element.name)) { + rule.reportLintForToken(node.name); + } + } + } + + @override + void visitFormalParameterList(FormalParameterList node) { + node.parameters.accept(this); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.returnType?.accept(this); + node.functionExpression.typeParameters?.accept(this); + node.functionExpression.parameters?.accept(this); + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.returnType?.accept(this); + node.typeParameters?.accept(this); + node.parameters.accept(this); + } + + @override + void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { + if (node.isNamed && Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.returnType?.accept(this); + node.typeParameters?.accept(this); + node.parameters.accept(this); + } + + @override + void visitGenericFunctionType(GenericFunctionType node) { + node.returnType?.accept(this); + node.typeParameters?.accept(this); + node.parameters.accept(this); + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.typeParameters?.accept(this); + node.functionType?.accept(this); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.returnType?.accept(this); + node.typeParameters?.accept(this); + node.parameters?.accept(this); + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + if (Identifier.isPrivateName(node.name.lexeme)) { + return; + } + node.onClause?.superclassConstraints.accept(this); + node.typeParameters?.accept(this); + node.members.accept(this); + } + + @override + void visitNamedType(NamedType node) { + var element = node.element; + if (element != null && isPrivate(element)) { + rule.reportLintForToken(node.name2); + } + node.typeArguments?.accept(this); + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter node) { + var name = node.name; + if (name != null && node.isNamed && Identifier.isPrivateName(name.lexeme)) { + return; + } + node.type?.accept(this); + } + + @override + void visitSuperFormalParameter(SuperFormalParameter node) { + if (node.isNamed && Identifier.isPrivateName(node.name.lexeme)) { + return; + } + + // Check for a declared type. + var type = node.type; + if (type != null) { + type.accept(this); + return; + } + + // Check implicit type. + var element = node.declaredElement; + if (element is SuperFormalParameterElement) { + var type = element.type; + if (type is InterfaceType && isPrivateName(type.element.name)) { + rule.reportLintForToken(node.name); + } + } + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + if (node.variables.variables + .any((field) => !Identifier.isPrivateName(field.name.lexeme))) { + node.variables.type?.accept(this); + } + } + + @override + void visitTypeArgumentList(TypeArgumentList node) { + node.arguments.accept(this); + } + + @override + void visitTypeParameter(TypeParameter node) { + node.bound?.accept(this); + } + + @override + void visitTypeParameterList(TypeParameterList node) { + node.typeParameters.accept(this); + } + + /// Return `true` if the given [element] is private or is defined in a private + /// library. + static bool isPrivate(Element element) => isPrivateName(element.name); + + static bool isPrivateName(String? name) => + name != null && Identifier.isPrivateName(name); +} + +class Visitor extends SimpleAstVisitor { + LintRule rule; + + Visitor(this.rule); + + @override + void visitCompilationUnit(CompilationUnit node) { + var element = node.declaredElement; + if (element != null && !Validator.isPrivate(element)) { + var validator = Validator(rule); + node.declarations.accept(validator); + } + } +} diff --git a/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart b/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart new file mode 100644 index 00000000000..33b5ae5a210 --- /dev/null +++ b/pkg/linter/lib/src/rules/lines_longer_than_80_chars.dart @@ -0,0 +1,232 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _cr = '\r'; + +const _desc = r'Avoid lines longer than 80 characters.'; + +const _details = r''' +**AVOID** lines longer than 80 characters + +Readability studies show that long lines of text are harder to read because your +eye has to travel farther when moving to the beginning of the next line. This is +why newspapers and magazines use multiple columns of text. + +If you really find yourself wanting lines longer than 80 characters, our +experience is that your code is likely too verbose and could be a little more +compact. The main offender is usually `VeryLongCamelCaseClassNames`. Ask +yourself, “Does each word in that type name tell me something critical or +prevent a name collision?” If not, consider omitting it. + +Note that `dart format` does 99% of this for you, but the last 1% is you. It +does not split long string literals to fit in 80 columns, so you have to do +that manually. + +We make an exception for URIs and file paths. When those occur in comments or +strings (usually in imports and exports), they may remain on a single line even +if they go over the line limit. This makes it easier to search source files for +a given path. +'''; + +const _lf = '\n'; + +/// String looks like URI if it contains a slash or backslash. +final _uriRegExp = RegExp(r'[/\\]'); +bool _looksLikeUriOrPath(String value) => _uriRegExp.hasMatch(value); + +class LinesLongerThan80Chars extends LintRule { + static const LintCode code = LintCode('lines_longer_than_80_chars', + 'The line length exceeds the 80-character limit.', + correctionMessage: 'Try breaking the line across multiple lines.'); + + LinesLongerThan80Chars() + : super( + name: 'lines_longer_than_80_chars', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _AllowedCommentVisitor extends SimpleAstVisitor { + final LineInfo lineInfo; + + final allowedLines = []; + _AllowedCommentVisitor(this.lineInfo); + + @override + void visitCompilationUnit(CompilationUnit node) { + Token? token = node.beginToken; + while (token != null) { + _getPrecedingComments(token).forEach(_visitComment); + if (token == token.next) break; + token = token.next; + } + } + + Iterable _getPrecedingComments(Token token) { + var tokens = []; + Token? comment = token.precedingComments; + while (comment != null) { + tokens.add(comment); + comment = comment.next; + } + return tokens; + } + + void _visitComment(Token comment) { + var content = comment.lexeme; + var lines = []; + if (content.startsWith('///')) { + lines.add(content.substring(3)); + } else if (content.startsWith('//')) { + var commentContent = content.substring(2); + if (commentContent.trimLeft().startsWith('ignore:')) { + allowedLines.add(lineInfo.getLocation(comment.offset).lineNumber); + } else { + lines.add(commentContent); + } + } else if (content.startsWith('/*')) { + // remove last slash before finding slash + lines.addAll(content + .substring(2, content.length - 2) + .split('$_cr$_lf') + .expand((e) => e.split(_cr)) + .expand((e) => e.split(_lf))); + } + for (var i = 0; i < lines.length; i++) { + var value = lines[i]; + if (_looksLikeUriOrPath(value)) { + var line = lineInfo.getLocation(comment.offset).lineNumber + i; + allowedLines.add(line); + } + } + } +} + +class _AllowedLongLineVisitor extends RecursiveAstVisitor { + final LineInfo lineInfo; + + final allowedLines = []; + _AllowedLongLineVisitor(this.lineInfo); + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (node.isMultiline) { + _handleMultilines(node); + } else { + _handleSingleLine(node, node.value); + } + } + + @override + void visitStringInterpolation(StringInterpolation node) { + if (node.isMultiline) { + _handleMultilines(node); + } else { + var value = node.elements.map((e) { + if (e is InterpolationString) return e.value; + if (e is InterpolationExpression) return ' ' * e.length; + throw ArgumentError( + 'Unhandled string interpolation element: ${node.runtimeType}'); + }).join(); + _handleSingleLine(node, value); + } + } + + void _handleMultilines(SingleStringLiteral node) { + var startLine = lineInfo.getLocation(node.offset).lineNumber; + var endLine = lineInfo.getLocation(node.end).lineNumber; + for (var i = startLine; i <= endLine; i++) { + allowedLines.add(i); + } + } + + void _handleSingleLine(AstNode node, String value) { + if (_looksLikeUriOrPath(value)) { + var line = lineInfo.getLocation(node.offset).lineNumber; + allowedLines.add(line); + } + } +} + +class _LineInfo { + final int index; + final int offset; + final int end; + _LineInfo({required this.index, required this.offset, required this.end}); + int get length => end - offset; +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCompilationUnit(CompilationUnit node) { + var lineInfo = node.lineInfo; + var lineCount = lineInfo.lineCount; + var longLines = <_LineInfo>[]; + for (var i = 0; i < lineCount; i++) { + var start = lineInfo.getOffsetOfLine(i); + int end; + if (i == lineCount - 1) { + end = node.end; + } else { + end = lineInfo.getOffsetOfLine(i + 1) - 1; + var length = end - start; + if (length > 80) { + if (context.currentUnit.content[end] == _lf && + context.currentUnit.content[end - 1] == _cr) { + end--; + } + } + } + var length = end - start; + if (length > 80) { + // Use 80 as the start of the range so that navigating to the lint + // will place the caret at exactly the location where the line needs + // to wrap. + var line = _LineInfo(index: i, offset: start + 80, end: end); + longLines.add(line); + } + } + + if (longLines.isEmpty) return; + + var allowedLineVisitor = _AllowedLongLineVisitor(lineInfo); + node.accept(allowedLineVisitor); + var allowedCommentVisitor = _AllowedCommentVisitor(lineInfo); + node.accept(allowedCommentVisitor); + + var allowedLines = [ + ...allowedLineVisitor.allowedLines, + ...allowedCommentVisitor.allowedLines + ]; + + for (var line in longLines) { + if (allowedLines.contains(line.index + 1)) continue; + rule.reporter + .reportErrorForOffset(rule.lintCode, line.offset, line.length); + } + } +} diff --git a/pkg/linter/lib/src/rules/list_remove_unrelated_type.dart b/pkg/linter/lib/src/rules/list_remove_unrelated_type.dart new file mode 100644 index 00000000000..7aca66fd367 --- /dev/null +++ b/pkg/linter/lib/src/rules/list_remove_unrelated_type.dart @@ -0,0 +1,160 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; +import '../util/unrelated_types_visitor.dart'; + +const _desc = r'Invocation of `remove` with references of unrelated types.'; + +const _details = r''' +**DON'T** invoke `remove` on `List` with an instance of different type than +the parameter type. + +Doing this will invoke `==` on its elements and most likely will +return `false`. + +**BAD:** +```dart +void someFunction() { + var list = []; + if (list.remove('1')) print('someFunction'); // LINT +} +``` + +**BAD:** +```dart +void someFunction3() { + List list = []; + if (list.remove('1')) print('someFunction3'); // LINT +} +``` + +**BAD:** +```dart +void someFunction8() { + List list = []; + DerivedClass3 instance; + if (list.remove(instance)) print('someFunction8'); // LINT +} +``` + +**BAD:** +```dart +abstract class SomeList implements List {} + +abstract class MyClass implements SomeList { + bool badMethod(String thing) => this.remove(thing); // LINT +} +``` + +**GOOD:** +```dart +void someFunction10() { + var list = []; + if (list.remove(1)) print('someFunction10'); // OK +} +``` + +**GOOD:** +```dart +void someFunction1() { + var list = []; + if (list.remove(1)) print('someFunction1'); // OK +} +``` + +**GOOD:** +```dart +void someFunction4() { + List list = []; + if (list.remove(1)) print('someFunction4'); // OK +} +``` + +**GOOD:** +```dart +void someFunction5() { + List list = []; + DerivedClass1 instance; + if (list.remove(instance)) print('someFunction5'); // OK +} + +abstract class ClassBase {} + +class DerivedClass1 extends ClassBase {} +``` + +**GOOD:** +```dart +void someFunction6() { + List list = []; + DerivedClass2 instance; + if (list.remove(instance)) print('someFunction6'); // OK +} + +abstract class ClassBase {} + +abstract class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} +``` + +**GOOD:** +```dart +void someFunction7() { + List list = []; + DerivedClass3 instance; + if (list.remove(instance)) print('someFunction7'); // OK +} + +abstract class ClassBase {} + +abstract class Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} +``` + +**DEPRECATED:** This rule is deprecated in favor of +`collection_methods_unrelated_type`. +The rule will be removed in a future Dart release. + +'''; + +class ListRemoveUnrelatedType extends LintRule { + static const LintCode code = LintCode('list_remove_unrelated_type', + "The argument type '{0}' isn't related to '{1}'."); + + ListRemoveUnrelatedType() + : super( + name: 'list_remove_unrelated_type', + description: _desc, + details: _details, + group: Group.errors, + state: + State.deprecated(replacedBy: 'collection_methods_unrelated_type'), + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context.typeSystem, context.typeProvider); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends UnrelatedTypesProcessors { + _Visitor(super.rule, super.typeSystem, super.typeProvider); + + @override + List get methods => [ + MethodDefinitionForElement( + typeProvider.listElement, + 'remove', + ExpectedArgumentKind.assignableToCollectionTypeArgument, + ), + ]; +} diff --git a/pkg/linter/lib/src/rules/literal_only_boolean_expressions.dart b/pkg/linter/lib/src/rules/literal_only_boolean_expressions.dart new file mode 100644 index 00000000000..fe6b2d75d36 --- /dev/null +++ b/pkg/linter/lib/src/rules/literal_only_boolean_expressions.dart @@ -0,0 +1,189 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Boolean expression composed only with literals.'; + +const _details = r''' +**DON'T** test for conditions composed only by literals, since the value can be +inferred at compile time. + +Conditional statements using a condition which cannot be anything but FALSE have +the effect of making blocks of code non-functional. If the condition cannot +evaluate to anything but `true`, the conditional statement is completely +redundant, and makes the code less readable. +It is quite likely that the code does not match the programmer's intent. +Either the condition should be removed or it should be updated so that it does +not always evaluate to `true` or `false`. + +**BAD:** +```dart +void bad() { + if (true) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (true && 1 != 0) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (1 != 0 && true) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (1 < 0 && true) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (true && false) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (1 != 0) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (true && 1 != 0 || 3 < 4) {} // LINT +} +``` + +**BAD:** +```dart +void bad() { + if (1 != 0 || 3 < 4 && true) {} // LINT +} +``` + +**NOTE:** that an exception is made for the common `while (true) { }` idiom, +which is often reasonably preferred to the equivalent `for (;;)`. + +**GOOD:** +```dart +void good() { + while (true) { + // Do stuff. + } +} +``` +'''; + +bool _onlyLiterals(Expression? rawExpression) { + var expression = rawExpression?.unParenthesized; + if (expression is Literal) { + return true; + } + if (expression is PrefixExpression) { + return _onlyLiterals(expression.operand); + } + if (expression is BinaryExpression) { + if (expression.operator.type == TokenType.QUESTION_QUESTION) { + return _onlyLiterals(expression.leftOperand); + } + return _onlyLiterals(expression.leftOperand) && + _onlyLiterals(expression.rightOperand); + } + return false; +} + +class LiteralOnlyBooleanExpressions extends LintRule { + static const LintCode code = LintCode('literal_only_boolean_expressions', + 'The Boolean expression has a constant value.', + correctionMessage: 'Try changing the expression.'); + + LiteralOnlyBooleanExpressions() + : super( + name: 'literal_only_boolean_expressions', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDoStatement(this, visitor); + registry.addForStatement(this, visitor); + registry.addIfStatement(this, visitor); + registry.addWhenClause(this, visitor); + registry.addWhileStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitDoStatement(DoStatement node) { + if (_onlyLiterals(node.condition)) { + rule.reportLint(node); + } + } + + @override + void visitForStatement(ForStatement node) { + var loopParts = node.forLoopParts; + if (loopParts is ForParts) { + if (_onlyLiterals(loopParts.condition)) { + rule.reportLint(node); + } + } + } + + @override + void visitIfStatement(IfStatement node) { + if (node.caseClause != null) return; + if (_onlyLiterals(node.expression)) { + rule.reportLint(node); + } + } + + @override + void visitWhenClause(WhenClause node) { + if (_onlyLiterals(node.expression)) { + rule.reportLint(node); + } + } + + @override + void visitWhileStatement(WhileStatement node) { + var condition = node.condition; + // Allow `while (true) { }` + // See: https://github.com/dart-lang/linter/issues/453 + if (condition is BooleanLiteral && condition.value) { + return; + } + + if (_onlyLiterals(condition)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/matching_super_parameters.dart b/pkg/linter/lib/src/rules/matching_super_parameters.dart new file mode 100644 index 00000000000..a991253d630 --- /dev/null +++ b/pkg/linter/lib/src/rules/matching_super_parameters.dart @@ -0,0 +1,135 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use matching super parameter names.'; + +const _details = r''' +**DO** use super parameter names that match their corresponding super +constructor's parameter names. + +**BAD:** + +```dart +class Rectangle { + final int width; + final int height; + + Rectangle(this.width, this.height); +} + +class ColoredRectangle extends Rectangle { + final Color color; + + ColoredRectangle( + this.color, + super.height, // Bad, actually corresponds to the `width` parameter. + super.width, // Bad, actually corresponds to the `height` parameter. + ); +} +``` + +**GOOD:** + +```dart +class Rectangle { + final int width; + final int height; + + Rectangle(this.width, this.height); +} + +class ColoredRectangle extends Rectangle { + final Color color; + + ColoredRectangle( + this.color, + super.width, + super.height, + ); +} +``` +'''; + +class MatchingSuperParameters extends LintRule { + static const LintCode code = LintCode( + 'matching_super_parameters', + "The super parameter named '{0}'' does not share the same name as the " + "corresponding parameter in the super constructor, '{1}'.", + correctionMessage: + 'Try using the name of the corresponding parameter in the super ' + 'constructor.'); + + MatchingSuperParameters() + : super( + name: 'matching_super_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + const _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var positionalSuperParameters = []; + for (var parameter in node.parameters.parameters) { + if (parameter is SuperFormalParameter && parameter.isPositional) { + positionalSuperParameters.add(parameter); + } + } + if (positionalSuperParameters.isEmpty) { + // We are only concerned with positional super-parameters. + return; + } + var superInvocation = + node.initializers.whereType().firstOrNull; + var superConstructor = superInvocation?.staticElement; + if (superConstructor == null) { + var class_ = node.parent; + if (class_ is ClassDeclaration) { + superConstructor = + class_.declaredElement?.supertype?.element.unnamedConstructor; + } + } + if (superConstructor is! ConstructorElement) { + return; + } + var positionalParametersOfSuper = + superConstructor.parameters.where((p) => p.isPositional).toList(); + if (positionalParametersOfSuper.length < positionalSuperParameters.length) { + // More positional parameters are passed to super constructor than it + // has positional parameters, an error. + return; + } + for (var i = 0; i < positionalSuperParameters.length; i++) { + var superParameter = positionalSuperParameters[i]; + var superParameterName = superParameter.name.lexeme; + var parameterOfSuperName = positionalParametersOfSuper[i].name; + if (superParameterName != parameterOfSuperName) { + rule.reportLint(superParameter, + arguments: [superParameterName, parameterOfSuperName]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/missing_whitespace_between_adjacent_strings.dart b/pkg/linter/lib/src/rules/missing_whitespace_between_adjacent_strings.dart new file mode 100644 index 00000000000..05666f936e3 --- /dev/null +++ b/pkg/linter/lib/src/rules/missing_whitespace_between_adjacent_strings.dart @@ -0,0 +1,154 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Missing whitespace between adjacent strings.'; + +const _details = r''' +Add a trailing whitespace to prevent missing whitespace between adjacent +strings. + +With long text split across adjacent strings it's easy to forget a whitespace +between strings. + +**BAD:** +```dart +var s = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed' + 'do eiusmod tempor incididunt ut labore et dolore magna'; +``` + +**GOOD:** +```dart +var s = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed ' + 'do eiusmod tempor incididunt ut labore et dolore magna'; +``` + +'''; + +class MissingWhitespaceBetweenAdjacentStrings extends LintRule { + static const LintCode code = LintCode( + 'missing_whitespace_between_adjacent_strings', + 'Missing whitespace between adjacent strings.', + correctionMessage: 'Try adding whitespace between the strings.'); + + MissingWhitespaceBetweenAdjacentStrings() + : super( + name: 'missing_whitespace_between_adjacent_strings', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAdjacentStrings(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAdjacentStrings(AdjacentStrings node) { + // Skip strings passed to `RegExp()` or any method named `matches`. + var parent = node.parent; + if (parent is ArgumentList) { + var parentParent = parent.parent; + if (_isRegExpInstanceCreation(parentParent) || + parentParent is MethodInvocation && + parentParent.realTarget == null && + const ['RegExp', 'matches'] + .contains(parentParent.methodName.name)) { + return; + } + } + + for (var i = 0; i < node.strings.length - 1; i++) { + var current = node.strings[i]; + var next = node.strings[i + 1]; + if (current.endsWithWhitespace || next.startsWithWhitespace) { + continue; + } + if (!current.hasWhitespace) { + continue; + } + rule.reportLint(current); + } + + return super.visitAdjacentStrings(node); + } + + static bool _isRegExpInstanceCreation(AstNode? node) { + if (node is InstanceCreationExpression) { + var constructorElement = node.constructorName.staticElement; + return constructorElement?.enclosingElement.name == 'RegExp'; + } + return false; + } +} + +extension on StringLiteral { + /// Returns whether this ends with whitespace, where an initial + /// [InterpolationExpression] counts as whitespace. + bool get endsWithWhitespace { + var self = this; + if (self is SimpleStringLiteral) { + return self.value.endsWithWhitespace; + } else if (self is StringInterpolation) { + var last = self.elements.last as InterpolationString; + return last.value.isEmpty || last.value.endsWithWhitespace; + } + throw ArgumentError( + 'Expected SimpleStringLiteral or StringInterpolation, but got ' + '$runtimeType'); + } + + /// Returns whether this contains whitespace, where any + /// [InterpolationExpression] does not count as whitespace. + bool get hasWhitespace { + var self = this; + if (self is SimpleStringLiteral) { + return self.value.hasWhitespace; + } else if (self is StringInterpolation) { + return self.elements + .any((e) => e is InterpolationString && e.value.hasWhitespace); + } + return false; + } + + /// Returns whether this starts with whitespace, where an initial + /// [InterpolationExpression] counts as whitespace. + bool get startsWithWhitespace { + var self = this; + if (self is SimpleStringLiteral) { + return self.value.startsWithWhitespace; + } else if (self is StringInterpolation) { + var first = self.elements.first as InterpolationString; + return first.value.isEmpty || first.value.startsWithWhitespace; + } + throw ArgumentError( + 'Expected SimpleStringLiteral or StringInterpolation, but got ' + '$runtimeType'); + } +} + +extension on String { + static const whitespaces = [' ', '\n', '\r', '\t']; + bool get endsWithWhitespace => whitespaces.any(endsWith); + bool get hasWhitespace => whitespaces.any(contains); + + bool get startsWithWhitespace => whitespaces.any(startsWith); +} diff --git a/pkg/linter/lib/src/rules/no_adjacent_strings_in_list.dart b/pkg/linter/lib/src/rules/no_adjacent_strings_in_list.dart new file mode 100644 index 00000000000..00c43cd754d --- /dev/null +++ b/pkg/linter/lib/src/rules/no_adjacent_strings_in_list.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't use adjacent strings in list."; + +const _details = r''' +**DON'T** use adjacent strings in a list. + +This can indicate a forgotten comma. + +**BAD:** +```dart +List list = [ + 'a' + 'b', + 'c', +]; +``` + +**GOOD:** +```dart +List list = [ + 'a' + + 'b', + 'c', +]; +``` + +'''; + +class NoAdjacentStringsInList extends LintRule { + static const LintCode code = LintCode('no_adjacent_strings_in_list', + "Don't use adjacent strings in a list literal.", + correctionMessage: 'Try adding a comma between the strings.'); + + NoAdjacentStringsInList() + : super( + name: 'no_adjacent_strings_in_list', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addForElement(this, visitor); + registry.addIfElement(this, visitor); + registry.addListLiteral(this, visitor); + registry.addSetOrMapLiteral(this, visitor); + registry.addSwitchPatternCase(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void check(AstNode? element) { + if (element is AdjacentStrings) { + rule.reportLint(element); + } + } + + @override + void visitForElement(ForElement node) { + if (node.body is AdjacentStrings) { + check(node.body); + } + } + + @override + void visitIfElement(IfElement node) { + if (node.elseElement == null && node.thenElement is AdjacentStrings) { + rule.reportLint(node.thenElement); + } else { + check(node.elseElement); + } + } + + @override + void visitListLiteral(ListLiteral node) { + node.elements.forEach(check); + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + if (node.isMap) return; + node.elements.forEach(check); + } + + @override + void visitSwitchPatternCase(SwitchPatternCase node) { + var pattern = node.guardedPattern.pattern.unParenthesized; + if (pattern is! ListPattern) return; + for (var element in pattern.elements) { + if (element is ConstantPattern) { + check(element.expression.unParenthesized); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/no_default_cases.dart b/pkg/linter/lib/src/rules/no_default_cases.dart new file mode 100644 index 00000000000..4749f0bdf07 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_default_cases.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'No default cases.'; + +const _details = r''' +Switches on enums and enum-like classes should not use a `default` clause. + +Enum-like classes are defined as concrete (non-abstract) classes that have: + * only private non-factory constructors + * two or more static const fields whose type is the enclosing class and + * no subclasses of the class in the defining library + +**DO** define default behavior outside switch statements. + +**BAD:** +```dart + switch (testEnum) { + case TestEnum.A: + return '123'; + case TestEnum.B: + return 'abc'; + default: + return null; + } +``` + +**GOOD:** +```dart + switch (testEnum) { + case TestEnum.A: + return '123'; + case TestEnum.B: + return 'abc'; + } + // Default here. + return null; +``` + +'''; + +class NoDefaultCases extends LintRule { + static const LintCode code = LintCode( + 'no_default_cases', "Invalid use of 'default' member in a switch.", + correctionMessage: + 'Try enumerating all the possible values of the switch expression.'); + + NoDefaultCases() + : super( + name: 'no_default_cases', + description: _desc, + details: _details, + group: Group.style, + state: State.experimental(), + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSwitchStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSwitchStatement(SwitchStatement statement) { + var expressionType = statement.expression.staticType; + if (expressionType is InterfaceType) { + for (var member in statement.members) { + if (member is SwitchDefault) { + var interfaceElement = expressionType.element; + if (interfaceElement is EnumElement || + interfaceElement is ClassElement && + interfaceElement.isEnumLikeClass) { + rule.reportLint(member); + } + return; + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/no_duplicate_case_values.dart b/pkg/linter/lib/src/rules/no_duplicate_case_values.dart new file mode 100644 index 00000000000..5033dd9499c --- /dev/null +++ b/pkg/linter/lib/src/rules/no_duplicate_case_values.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/constant/value.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't use more than one case with same value."; + +const _details = r''' +**DON'T** use more than one case with same value. + +This is usually a typo or changed value of constant. + +**BAD:** +```dart +const int A = 1; +switch (v) { + case 1: + case 2: + case A: + case 2: +} +``` + +**GOOD:** +```dart +const int A = 1; +switch (v) { + case A: + case 2: +} +``` + +NOTE: this lint only reports duplicate cases in libraries opted in to Dart 2.19 +and below. In Dart 3.0 and after, duplicate cases are reported as dead code +by the analyzer. +'''; + +class NoDuplicateCaseValues extends LintRule { + static const LintCode code = LintCode( + 'no_duplicate_case_values', + "The value of the case clause ('{0}') is equal to the value of an " + "earlier case clause ('{1}').", + correctionMessage: 'Try removing or changing the value.'); + + NoDuplicateCaseValues() + : super( + name: 'no_duplicate_case_values', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addSwitchStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final NoDuplicateCaseValues rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitSwitchStatement(SwitchStatement node) { + var values = {}; + + for (var member in node.members) { + if (member is SwitchCase) { + var expression = member.expression; + + var result = context.evaluateConstant(expression); + var value = result.value; + + if (value == null || !value.hasKnownValue) { + continue; + } + + var duplicateValue = values[value]; + // TODO(brianwilkeson) This would benefit from having a context message + // pointing at the `duplicateValue`. + if (duplicateValue != null) { + rule.reportLint(expression, + arguments: [expression.toString(), duplicateValue.toString()]); + } else { + values[value] = expression; + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/no_leading_underscores_for_library_prefixes.dart b/pkg/linter/lib/src/rules/no_leading_underscores_for_library_prefixes.dart new file mode 100644 index 00000000000..e754fdf144f --- /dev/null +++ b/pkg/linter/lib/src/rules/no_leading_underscores_for_library_prefixes.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Avoid leading underscores for library prefixes.'; + +const _details = r''' +**DON'T** use a leading underscore for library prefixes. +There is no concept of "private" for library prefixes. When one of those has a +name that starts with an underscore, it sends a confusing signal to the reader. +To avoid that, don't use leading underscores in those names. + +**BAD:** +```dart +import 'dart:core' as _core; +``` + +**GOOD:** +```dart +import 'dart:core' as core; +``` +'''; + +class NoLeadingUnderscoresForLibraryPrefixes extends LintRule { + static const LintCode code = LintCode( + 'no_leading_underscores_for_library_prefixes', + "The library prefix '{0}' starts with an underscore.", + correctionMessage: + 'Try renaming the prefix to not start with an underscore.'); + + NoLeadingUnderscoresForLibraryPrefixes() + : super( + name: 'no_leading_underscores_for_library_prefixes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkIdentifier(SimpleIdentifier? id) { + if (id == null) { + return; + } + + if (id.name.hasLeadingUnderscore) { + rule.reportLint(id, arguments: [id.name]); + } + } + + @override + void visitImportDirective(ImportDirective node) { + checkIdentifier(node.prefix); + } +} diff --git a/pkg/linter/lib/src/rules/no_leading_underscores_for_local_identifiers.dart b/pkg/linter/lib/src/rules/no_leading_underscores_for_local_identifiers.dart new file mode 100644 index 00000000000..ab452d5f4c7 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_leading_underscores_for_local_identifiers.dart @@ -0,0 +1,149 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Avoid leading underscores for local identifiers.'; + +const _details = r''' +**DON'T** use a leading underscore for identifiers that aren't private. Dart +uses a leading underscore in an identifier to mark members and top-level +declarations as private. This trains users to associate a leading underscore +with one of those kinds of declarations. They see `_` and think "private". +There is no concept of "private" for local variables or parameters. When one of +those has a name that starts with an underscore, it sends a confusing signal to +the reader. To avoid that, don't use leading underscores in those names. + +**EXCEPTION:**: An unused parameter can be named `_`, `__`, `___`, etc. This is +common practice in callbacks where you are passed a value but you don't need +to use it. Giving it a name that consists solely of underscores is the idiomatic +way to indicate that the value isn't used. + +**BAD:** +```dart +void print(String _name) { + var _size = _name.length; + ... +} +``` +**GOOD:** + +```dart +void print(String name) { + var size = name.length; + ... +} +``` + +**OK:** + +```dart +[1,2,3].map((_) => print('Hello')); +``` +'''; + +class NoLeadingUnderscoresForLocalIdentifiers extends LintRule { + static const LintCode code = LintCode( + 'no_leading_underscores_for_local_identifiers', + "The local variable '{0}' starts with an underscore.", + correctionMessage: + 'Try renaming the variable to not start with an underscore.'); + + NoLeadingUnderscoresForLocalIdentifiers() + : super( + name: 'no_leading_underscores_for_local_identifiers', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCatchClause(this, visitor); + registry.addDeclaredIdentifier(this, visitor); + registry.addFormalParameterList(this, visitor); + registry.addForPartsWithDeclarations(this, visitor); + registry.addFunctionDeclarationStatement(this, visitor); + registry.addDeclaredVariablePattern(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkIdentifier(Token? id) { + if (id == null) return; + if (!id.lexeme.hasLeadingUnderscore) return; + if (id.lexeme.isJustUnderscores) return; + + rule.reportLintForToken(id, arguments: [id.lexeme]); + } + + @override + void visitCatchClause(CatchClause node) { + checkIdentifier(node.exceptionParameter?.name); + checkIdentifier(node.stackTraceParameter?.name); + } + + @override + void visitDeclaredIdentifier(DeclaredIdentifier node) { + checkIdentifier(node.name); + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + if (node.parent.isFieldNameShortcut) return; + checkIdentifier(node.name); + } + + @override + void visitFormalParameterList(FormalParameterList node) { + for (var parameter in node.parameters) { + if (parameter is DefaultFormalParameter) { + parameter = parameter.parameter; + } + if (parameter is FieldFormalParameter || + parameter is SuperFormalParameter) { + // These are not local identifiers. + return; + } + if (!parameter.isNamed) { + // Named parameters produce a `private_optional_parameter` diagnostic. + checkIdentifier(parameter.name); + } + } + } + + @override + void visitForPartsWithDeclarations(ForPartsWithDeclarations node) { + for (var variable in node.variables.variables) { + checkIdentifier(variable.name); + } + } + + @override + void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { + checkIdentifier(node.functionDeclaration.name); + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + for (var variable in node.variables.variables) { + checkIdentifier(variable.name); + } + } +} diff --git a/pkg/linter/lib/src/rules/no_literal_bool_comparisons.dart b/pkg/linter/lib/src/rules/no_literal_bool_comparisons.dart new file mode 100644 index 00000000000..e6fbb0c4ab0 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_literal_bool_comparisons.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't compare booleans to boolean literals."; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#dont-use-true-or-false-in-equality-operations): + +**DON'T** use `true` or `false` in equality operations. + +This lint applies only if the expression is of a non-nullable `bool` type. + +**BAD:** +```dart +if (someBool == true) { +} +while (someBool == false) { +} +``` + +**GOOD:** +```dart +if (someBool) { +} +while (!someBool) { +} +``` +'''; + +class NoLiteralBoolComparisons extends LintRule { + NoLiteralBoolComparisons() + : super( + name: 'no_literal_bool_comparisons', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) return; + + var visitor = _Visitor(this, context); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitBinaryExpression(BinaryExpression node) { + if (node.operator.type == TokenType.EQ_EQ || + node.operator.type == TokenType.BANG_EQ) { + var left = node.leftOperand; + var right = node.rightOperand; + if (right is BooleanLiteral && isBool(left.staticType)) { + rule.reportLint(right); + } else if (left is BooleanLiteral && isBool(right.staticType)) { + rule.reportLint(left); + } + } + } + + bool isBool(DartType? type) => + type != null && + type.isDartCoreBool && + context.typeSystem.isNonNullable(type); +} diff --git a/pkg/linter/lib/src/rules/no_logic_in_create_state.dart b/pkg/linter/lib/src/rules/no_logic_in_create_state.dart new file mode 100644 index 00000000000..540df584b2b --- /dev/null +++ b/pkg/linter/lib/src/rules/no_logic_in_create_state.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r"Don't put any logic in createState."; + +const _details = r''' +**DON'T** put any logic in `createState()`. + +Implementations of `createState()` should return a new instance +of a State object and do nothing more. Since state access is preferred +via the `widget` field, passing data to `State` objects using custom +constructor parameters should also be avoided and so further, the State +constructor is required to be passed no arguments. + +**BAD:** +```dart +MyState global; + +class MyStateful extends StatefulWidget { + @override + MyState createState() { + global = MyState(); + return global; + } +} +``` + +```dart +class MyStateful extends StatefulWidget { + @override + MyState createState() => MyState()..field = 42; +} +``` + +```dart +class MyStateful extends StatefulWidget { + @override + MyState createState() => MyState(42); +} +``` + + +**GOOD:** +```dart +class MyStateful extends StatefulWidget { + @override + MyState createState() { + return MyState(); + } +} +``` +'''; + +class NoLogicInCreateState extends LintRule { + static const LintCode code = LintCode( + 'no_logic_in_create_state', "Don't put any logic in 'createState'.", + correctionMessage: "Try moving the logic out of 'createState'."); + + NoLogicInCreateState() + : super( + name: 'no_logic_in_create_state', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.name.lexeme != 'createState') { + return; + } + + var parent = node.parent; + if (parent is! ClassDeclaration || + !isStatefulWidget(parent.declaredElement)) { + return; + } + var body = node.body; + Expression? expressionToTest; + if (body is BlockFunctionBody) { + var statements = body.block.statements; + if (statements.length == 1) { + var statement = statements.first; + if (statement is ReturnStatement) { + expressionToTest = statement.expression; + } + } + } else if (body is ExpressionFunctionBody) { + expressionToTest = body.expression; + } else if (body is EmptyFunctionBody) { + return; + } + + if (expressionToTest is InstanceCreationExpression) { + if (expressionToTest.argumentList.arguments.isEmpty) { + return; + } + } + rule.reportLint(expressionToTest ?? body); + } +} diff --git a/pkg/linter/lib/src/rules/no_runtimeType_toString.dart b/pkg/linter/lib/src/rules/no_runtimeType_toString.dart new file mode 100644 index 00000000000..eeab2d013e4 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_runtimeType_toString.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid calling toString() on runtimeType.'; + +const _details = r''' +Calling `toString` on a runtime type is a non-trivial operation that can +negatively impact performance. It's better to avoid it. + +**BAD:** +```dart +class A { + String toString() => '$runtimeType()'; +} +``` + +**GOOD:** +```dart +class A { + String toString() => 'A()'; +} +``` + +This lint has some exceptions where performance is not a problem or where real +type information is more important than performance: + +* in assertion +* in throw expressions +* in catch clauses +* in mixin declaration +* in abstract class + +'''; + +class NoRuntimeTypeToString extends LintRule { + static const LintCode code = LintCode('no_runtimeType_toString', + "Using 'toString' on a 'Type' is not safe in production code."); + + NoRuntimeTypeToString() + : super( + name: 'no_runtimeType_toString', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInterpolationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInterpolationExpression(InterpolationExpression node) { + if (_canSkip(node)) { + return; + } + if (_isRuntimeTypeAccess(node.expression)) { + rule.reportLint(node.expression); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (_canSkip(node)) { + return; + } + if (node.methodName.name == 'toString' && + _isRuntimeTypeAccess(node.realTarget)) { + rule.reportLint(node.methodName); + } + } + + bool _canSkip(AstNode node) => + node.thisOrAncestorMatching((n) { + if (n is Assertion) return true; + if (n is ThrowExpression) return true; + if (n is CatchClause) return true; + if (n is MixinDeclaration) return true; + if (n is ClassDeclaration && n.abstractKeyword != null) return true; + if (n is ExtensionDeclaration) { + var declaredElement = n.declaredElement; + if (declaredElement != null) { + var extendedType = declaredElement.extendedType; + if (extendedType is InterfaceType) { + var extendedElement = extendedType.element; + return !(extendedElement is ClassElement && + !extendedElement.isAbstract); + } + } + } + return false; + }) != + null; + + bool _isRuntimeTypeAccess(Expression? target) => + target is PropertyAccess && + (target.target is ThisExpression || + target.target is SuperExpression) && + target.propertyName.name == 'runtimeType' || + target is SimpleIdentifier && + target.name == 'runtimeType' && + target.staticElement is PropertyAccessorElement; +} diff --git a/pkg/linter/lib/src/rules/no_self_assignments.dart b/pkg/linter/lib/src/rules/no_self_assignments.dart new file mode 100644 index 00000000000..d7aea6d92c6 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_self_assignments.dart @@ -0,0 +1,154 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import '../analyzer.dart'; + +const _desc = r"Don't assign a variable to itself."; + +const _details = r''' +**DON'T** assign a variable to itself. Usually this is a mistake. + +**BAD:** +```dart +class C { + int x; + + C(int x) { + x = x; + } +} +``` + +**GOOD:** +```dart +class C { + int x; + + C(int x) : x = x; +} +``` + +**GOOD:** +```dart +class C { + int x; + + C(int x) { + this.x = x; + } +} +``` + +**BAD:** +```dart +class C { + int _x = 5; + + int get x => _x; + + set x(int x) { + _x = x; + _customUpdateLogic(); + } + + void _customUpdateLogic() { + print('updated'); + } + + void example() { + x = x; + } +} +``` + +**GOOD:** +```dart +class C { + int _x = 5; + + int get x => _x; + + set x(int x) { + _x = x; + _customUpdateLogic(); + } + + void _customUpdateLogic() { + print('updated'); + } + + void example() { + _customUpdateLogic(); + } +} +``` + +**BAD:** +```dart +class C { + int x = 5; + + void update(C other) { + this.x = this.x; + } +} +``` + +**GOOD:** +```dart +class C { + int x = 5; + + void update(C other) { + this.x = other.x; + } +} +``` + +'''; + +class NoSelfAssignments extends LintRule { + static const LintCode code = LintCode('no_self_assignments', + 'The variable or property is being assigned to itself.', + correctionMessage: + 'Try removing the assignment that has no direct effect.'); + + NoSelfAssignments() + : super( + name: 'no_self_assignments', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAssignmentExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.operator.type != TokenType.EQ) return; + var lhs = node.leftHandSide; + var rhs = node.rightHandSide; + if (lhs is Identifier && rhs is Identifier) { + if (lhs.name == rhs.name) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/no_wildcard_variable_uses.dart b/pkg/linter/lib/src/rules/no_wildcard_variable_uses.dart new file mode 100644 index 00000000000..fb762cc8564 --- /dev/null +++ b/pkg/linter/lib/src/rules/no_wildcard_variable_uses.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r"Don't use wildcard parameters or variables."; + +const _details = r''' +**DON'T** use wildcard parameters or variables. + +Wildcard parameters and local variables +(e.g. underscore-only names like `_`, `__`, `___`, etc.) will +become non-binding in a future version of the Dart language. +Any existing code that uses wildcard parameters or variables will +break. In anticipation of this change, and to make adoption easier, +this lint disallows wildcard and variable parameter uses. + + +**BAD:** +```dart +var _ = 1; +print(_); // LINT +``` + +```dart +void f(int __) { + print(__); // LINT multiple underscores too +} +``` + +**GOOD:** +```dart +for (var _ in [1, 2, 3]) count++; +``` + +```dart +var [a, _, b, _] = [1, 2, 3, 4]; +``` +'''; + +class NoWildcardVariableUses extends LintRule { + static const LintCode code = LintCode( + 'no_wildcard_variable_uses', 'The referenced identifier is a wildcard.', + correctionMessage: 'Use an identifier name that is not a wildcard.'); + + NoWildcardVariableUses() + : super( + name: 'no_wildcard_variable_uses', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSimpleIdentifier(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + var element = node.staticElement; + if (element is! LocalVariableElement && element is! ParameterElement) { + return; + } + + if (node.name.isJustUnderscores) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/non_constant_identifier_names.dart b/pkg/linter/lib/src/rules/non_constant_identifier_names.dart new file mode 100644 index 00000000000..d49173db433 --- /dev/null +++ b/pkg/linter/lib/src/rules/non_constant_identifier_names.dart @@ -0,0 +1,187 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/ascii_utils.dart'; +import '../utils.dart'; + +const _desc = r'Name non-constant identifiers using lowerCamelCase.'; + +const _details = r''' +**DO** name non-constant identifiers using lowerCamelCase. + +Class members, top-level definitions, variables, parameters, named parameters +and named constructors should capitalize the first letter of each word +except the first word, and use no separators. + +**GOOD:** +```dart +var item; + +HttpRequest httpRequest; + +align(clearItems) { + // ... +} +``` + +'''; + +class NonConstantIdentifierNames extends LintRule { + static const LintCode code = LintCode('non_constant_identifier_names', + "The variable name '{0}' isn't a lowerCamelCase identifier.", + correctionMessage: + 'Try changing the name to follow the lowerCamelCase style.'); + + NonConstantIdentifierNames() + : super( + name: 'non_constant_identifier_names', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addCatchClause(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addDeclaredVariablePattern(this, visitor); + registry.addExtensionTypeDeclaration(this, visitor); + registry.addForEachPartsWithDeclaration(this, visitor); + registry.addFormalParameterList(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addPatternField(this, visitor); + registry.addRecordLiteral(this, visitor); + registry.addRecordTypeAnnotation(this, visitor); + registry.addVariableDeclaration(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkIdentifier(Token? id, {bool underscoresOk = false}) { + if (id == null) { + return; + } + var name = id.lexeme; + if (underscoresOk && name.isJustUnderscores) { + // For example, `___` is OK in a callback. + return; + } + if (!isLowerCamelCase(name)) { + rule.reportLintForToken(id, arguments: [name]); + } + } + + @override + void visitCatchClause(CatchClause node) { + checkIdentifier(node.exceptionParameter?.name, underscoresOk: true); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + // For rationale on accepting underscores, see: + // https://github.com/dart-lang/linter/issues/1854 + checkIdentifier(node.name, underscoresOk: true); + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + if (node.parent.isFieldNameShortcut) return; + checkIdentifier(node.name); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + checkIdentifier(node.representation.constructorName?.name); + } + + @override + void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { + checkIdentifier(node.loopVariable.name); + } + + @override + void visitFormalParameterList(FormalParameterList node) { + for (var p in node.parameters) { + if (p is! FieldFormalParameter) { + checkIdentifier(p.name, underscoresOk: true); + } + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + checkIdentifier(node.name); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (!node.isOperator) { + checkIdentifier(node.name); + } + } + + @override + void visitPatternField(PatternField node) { + if (node.isFieldNameShortcut) return; + var pattern = node.pattern; + if (pattern is DeclaredVariablePattern) { + checkIdentifier(pattern.name); + } + } + + @override + void visitRecordLiteral(RecordLiteral node) { + for (var fieldExpression in node.fields) { + if (fieldExpression is NamedExpression) { + checkIdentifier(fieldExpression.name.label.token); + } + } + } + + @override + void visitRecordTypeAnnotation(RecordTypeAnnotation node) { + var positionalFields = node.positionalFields; + for (var field in positionalFields) { + checkIdentifier(field.name); + } + + var namedFields = node.namedFields; + if (namedFields == null) return; + for (var field in namedFields.fields) { + checkIdentifier(field.name); + } + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (!node.isConst) { + checkIdentifier(node.name); + } + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + for (var variable in node.variables.variables) { + if (!variable.isConst) { + checkIdentifier(variable.name); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/noop_primitive_operations.dart b/pkg/linter/lib/src/rules/noop_primitive_operations.dart new file mode 100644 index 00000000000..0627c95f73a --- /dev/null +++ b/pkg/linter/lib/src/rules/noop_primitive_operations.dart @@ -0,0 +1,128 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Noop primitive operations.'; + +const _details = r''' +Some operations on primitive types are idempotent and can be removed. + +**BAD:** + +```dart +doubleValue.toDouble(); + +intValue.toInt(); +intValue.round(); +intValue.ceil(); +intValue.floor(); +intValue.truncate(); + +string.toString(); +string = 'hello\n' + 'world\n' + ''; // useless empty string + +'string with ${x.toString()}'; +``` +'''; + +class NoopPrimitiveOperations extends LintRule { + static const LintCode code = LintCode('noop_primitive_operations', + 'The expression has no effect and can be removed.', + correctionMessage: 'Try removing the expression.'); + + NoopPrimitiveOperations() + : super( + name: 'noop_primitive_operations', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this, context); + registry.addAdjacentStrings(this, visitor); + registry.addInterpolationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitAdjacentStrings(AdjacentStrings node) { + for (var literal in node.strings) { + if (literal.stringValue?.isEmpty ?? false) { + rule.reportLint(literal); + } + } + } + + @override + void visitInterpolationExpression(InterpolationExpression node) { + _checkToStringInvocation(node.expression); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var type = node.realTarget?.staticType; + if (type == null) { + // print(xxx.toString()) + if (node.methodName.staticElement.isDartCorePrint && + node.argumentList.arguments.length == 1) { + _checkToStringInvocation(node.argumentList.arguments.first); + } + return; + } + + // string.toString() + if (type.isDartCoreString && + node.methodName.name == 'toString' && + context.typeSystem.isNonNullable(type)) { + rule.reportLint(node.methodName); + return; + } + + // int invariant methods + if (type.isDartCoreInt && + ['toInt', 'round', 'ceil', 'floor', 'truncate'] + .contains(node.methodName.name)) { + rule.reportLint(node.methodName); + return; + } + + // double.toDouble() + if (type.isDartCoreDouble && node.methodName.name == 'toDouble') { + rule.reportLint(node.methodName); + return; + } + } + + void _checkToStringInvocation(Expression expression) { + if (expression is MethodInvocation && + expression.realTarget != null && + expression.realTarget is! SuperExpression && + expression.methodName.name == 'toString' && + expression.argumentList.arguments.isEmpty) { + rule.reportLint(expression.methodName); + } + } +} diff --git a/pkg/linter/lib/src/rules/null_check_on_nullable_type_parameter.dart b/pkg/linter/lib/src/rules/null_check_on_nullable_type_parameter.dart new file mode 100644 index 00000000000..b5f15a9b8fe --- /dev/null +++ b/pkg/linter/lib/src/rules/null_check_on_nullable_type_parameter.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import 'unnecessary_null_checks.dart'; + +const _desc = r"Don't use null check on a potentially nullable type parameter."; + +const _details = r''' +**DON'T** use null check on a potentially nullable type parameter. + +Given a generic type parameter `T` which has a nullable bound (e.g. the default +bound of `Object?`), it is very easy to introduce erroneous null checks when +working with a variable of type `T?`. Specifically, it is not uncommon to have +`T? x;` and want to assert that `x` has been set to a valid value of type `T`. +A common mistake is to do so using `x!`. This is almost always incorrect, since +if `T` is a nullable type, `x` may validly hold `null` as a value of type `T`. + +**BAD:** +```dart +T run(T callback()) { + T? result; + (() { result = callback(); })(); + return result!; +} +``` + +**GOOD:** +```dart +T run(T callback()) { + T? result; + (() { result = callback(); })(); + return result as T; +} +``` + +'''; + +class NullCheckOnNullableTypeParameter extends LintRule { + static const LintCode code = LintCode( + 'null_check_on_nullable_type_parameter', + "The null check operator shouldn't be used on a variable whose type is a " + 'potentially nullable type parameter.', + correctionMessage: "Try explicitly testing for 'null'."); + + NullCheckOnNullableTypeParameter() + : super( + name: 'null_check_on_nullable_type_parameter', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addPostfixExpression(this, visitor); + registry.addNullAssertPattern(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + bool isNullableTypeParameterType(DartType? type) => + type is TypeParameterType && context.typeSystem.isNullable(type); + + @override + void visitNullAssertPattern(NullAssertPattern node) { + if (isNullableTypeParameterType(node.matchedValueType)) { + rule.reportLintForToken(node.operator); + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + if (node.operator.type != TokenType.BANG) return; + + var expectedType = getExpectedType(node); + var type = node.operand.staticType; + if (isNullableTypeParameterType(type) && + expectedType != null && + context.typeSystem.isPotentiallyNullable(expectedType) && + context.typeSystem.promoteToNonNull(type!) == + context.typeSystem.promoteToNonNull(expectedType)) { + rule.reportLintForToken(node.operator); + } + } +} diff --git a/pkg/linter/lib/src/rules/null_closures.dart b/pkg/linter/lib/src/rules/null_closures.dart new file mode 100644 index 00000000000..8daadc4e437 --- /dev/null +++ b/pkg/linter/lib/src/rules/null_closures.dart @@ -0,0 +1,329 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Do not pass `null` as an argument where a closure is expected.'; + +const _details = r''' +**DON'T** pass `null` as an argument where a closure is expected. + +Often a closure that is passed to a method will only be called conditionally, +so that tests and "happy path" production calls do not reveal that `null` will +result in an exception being thrown. + +This rule only catches null literals being passed where closures are expected +in the following locations: + +#### Constructors + +* From `dart:async` + * `Future` at the 0th positional parameter + * `Future.microtask` at the 0th positional parameter + * `Future.sync` at the 0th positional parameter + * `Timer` at the 0th positional parameter + * `Timer.periodic` at the 1st positional parameter +* From `dart:core` + * `List.generate` at the 1st positional parameter + +#### Static functions + +* From `dart:async` + * `scheduleMicrotask` at the 0th positional parameter + * `Future.doWhile` at the 0th positional parameter + * `Future.forEach` at the 0th positional parameter + * `Future.wait` at the named parameter `cleanup` + * `Timer.run` at the 0th positional parameter + +#### Instance methods + +* From `dart:async` + * `Future.then` at the 0th positional parameter + * `Future.complete` at the 0th positional parameter +* From `dart:collection` + * `Queue.removeWhere` at the 0th positional parameter + * `Queue.retain + * `Iterable.firstWhere` at the 0th positional parameter, and the named + parameter `orElse` + * `Iterable.forEach` at the 0th positional parameter + * `Iterable.fold` at the 1st positional parameter + * `Iterable.lastWhere` at the 0th positional parameter, and the named + parameter `orElse` + * `Iterable.map` at the 0th positional parameter + * `Iterable.reduce` at the 0th positional parameter + * `Iterable.singleWhere` at the 0th positional parameter, and the named + parameter `orElse` + * `Iterable.skipWhile` at the 0th positional parameter + * `Iterable.takeWhile` at the 0th positional parameter + * `Iterable.where` at the 0th positional parameter + * `List.removeWhere` at the 0th positional parameter + * `List.retainWhere` at the 0th positional parameter + * `String.replaceAllMapped` at the 1st positional parameter + * `String.replaceFirstMapped` at the 1st positional parameter + * `String.splitMapJoin` at the named parameters `onMatch` and `onNonMatch` + +**BAD:** +```dart +[1, 3, 5].firstWhere((e) => e.isOdd, orElse: null); +``` + +**GOOD:** +```dart +[1, 3, 5].firstWhere((e) => e.isOdd, orElse: () => null); +``` + +'''; + +List _constructorsWithNonNullableArguments = + [ + NonNullableFunction('dart.async', 'Future', null, positional: [0]), + NonNullableFunction('dart.async', 'Future', 'microtask', positional: [0]), + NonNullableFunction('dart.async', 'Future', 'sync', positional: [0]), + NonNullableFunction('dart.async', 'Timer', null, positional: [1]), + NonNullableFunction('dart.async', 'Timer', 'periodic', positional: [1]), + NonNullableFunction('dart.core', 'List', 'generate', positional: [1]), +]; + +final Map> + _instanceMethodsWithNonNullableArguments = { + 'any': { + NonNullableFunction('dart.core', 'Iterable', 'any', positional: [0]), + }, + 'complete': { + NonNullableFunction('dart.async', 'Future', 'complete', positional: [0]), + }, + 'every': { + NonNullableFunction('dart.core', 'Iterable', 'every', positional: [0]), + }, + 'expand': { + NonNullableFunction('dart.core', 'Iterable', 'expand', positional: [0]), + }, + 'firstWhere': { + NonNullableFunction('dart.core', 'Iterable', 'firstWhere', + positional: [0], named: ['orElse']), + }, + 'forEach': { + NonNullableFunction('dart.core', 'Iterable', 'forEach', positional: [0]), + NonNullableFunction('dart.core', 'Map', 'forEach', positional: [0]), + }, + 'fold': { + NonNullableFunction('dart.core', 'Iterable', 'fold', positional: [1]), + }, + 'lastWhere': { + NonNullableFunction('dart.core', 'Iterable', 'lastWhere', + positional: [0], named: ['orElse']), + }, + 'map': { + NonNullableFunction('dart.core', 'Iterable', 'map', positional: [0]), + }, + 'putIfAbsent': { + NonNullableFunction('dart.core', 'Map', 'putIfAbsent', positional: [1]), + }, + 'reduce': { + NonNullableFunction('dart.core', 'Iterable', 'reduce', positional: [0]), + }, + 'removeWhere': { + NonNullableFunction('dart.collection', 'Queue', 'removeWhere', + positional: [0]), + NonNullableFunction('dart.core', 'List', 'removeWhere', positional: [0]), + NonNullableFunction('dart.core', 'Set', 'removeWhere', positional: [0]), + }, + 'replaceAllMapped': { + NonNullableFunction('dart.core', 'String', 'replaceAllMapped', + positional: [1]), + }, + 'replaceFirstMapped': { + NonNullableFunction('dart.core', 'String', 'replaceFirstMapped', + positional: [1]), + }, + 'retainWhere': { + NonNullableFunction('dart.collection', 'Queue', 'retainWhere', + positional: [0]), + NonNullableFunction('dart.core', 'List', 'retainWhere', positional: [0]), + NonNullableFunction('dart.core', 'Set', 'retainWhere', positional: [0]), + }, + 'singleWhere': { + NonNullableFunction('dart.core', 'Iterable', 'singleWhere', + positional: [0], named: ['orElse']), + }, + 'skipWhile': { + NonNullableFunction('dart.core', 'Iterable', 'skipWhile', positional: [0]), + }, + 'splitMapJoin': { + NonNullableFunction('dart.core', 'String', 'splitMapJoin', + named: ['onMatch', 'onNonMatch']), + }, + 'takeWhile': { + NonNullableFunction('dart.core', 'Iterable', 'takeWhile', positional: [0]), + }, + 'then': { + NonNullableFunction('dart.async', 'Future', 'then', + positional: [0], named: ['onError']), + }, + 'where': { + NonNullableFunction('dart.core', 'Iterable', 'where', positional: [0]), + }, +}; + +List _staticFunctionsWithNonNullableArguments = + [ + NonNullableFunction('dart.async', null, 'scheduleMicrotask', positional: [0]), + NonNullableFunction('dart.async', 'Future', 'doWhile', positional: [0]), + NonNullableFunction('dart.async', 'Future', 'forEach', positional: [1]), + NonNullableFunction('dart.async', 'Future', 'wait', named: ['cleanUp']), + NonNullableFunction('dart.async', 'Timer', 'run', positional: [0]), +]; + +/// Function with closure parameters that cannot accept null arguments. +class NonNullableFunction { + final String library; + final String? type; + final String? name; + final List positional; + final List named; + + NonNullableFunction(this.library, this.type, this.name, + {this.positional = const [], this.named = const []}); + + @override + int get hashCode => + Object.hash(library.hashCode, type.hashCode, name.hashCode); + + /// Two [NonNullableFunction] objects are equal if their [library], [type], + /// and [name] are equal, for the purpose of discovering whether a function + /// invocation is among a collection of non-nullable functions. + @override + bool operator ==(Object other) => + other is NonNullableFunction && other.hashCode == hashCode; +} + +class NullClosures extends LintRule { + static const LintCode code = LintCode( + 'null_closures', "Closure can't be 'null' because it might be invoked.", + correctionMessage: 'Try providing a non-null closure.'); + + NullClosures() + : super( + name: 'null_closures', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var constructorName = node.constructorName; + var type = node.staticType; + for (var constructor in _constructorsWithNonNullableArguments) { + if (constructorName.name?.name == constructor.name) { + if (type.extendsClass(constructor.type, constructor.library)) { + _checkNullArgForClosure( + node.argumentList, constructor.positional, constructor.named); + } + } + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var target = node.target; + var methodName = node.methodName.name; + var element = target is Identifier ? target.staticElement : null; + if (element is ClassElement) { + // Static function called, "target" is the class. + for (var function in _staticFunctionsWithNonNullableArguments) { + if (methodName == function.name) { + if (element.name == function.type) { + _checkNullArgForClosure( + node.argumentList, function.positional, function.named); + } + } + } + } else { + // Instance method called, "target" is the instance. + var targetType = target?.staticType; + var method = _getInstanceMethod(targetType, methodName); + if (method == null) { + return; + } + _checkNullArgForClosure( + node.argumentList, method.positional, method.named); + } + } + + void _checkNullArgForClosure( + ArgumentList node, List positions, List names) { + var args = node.arguments; + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + + if (arg is NamedExpression) { + if (arg.expression is NullLiteral && + names.contains(arg.name.label.name)) { + rule.reportLint(arg); + } + } else { + if (arg is NullLiteral && positions.contains(i)) { + rule.reportLint(arg); + } + } + } + } + + NonNullableFunction? _getInstanceMethod(DartType? type, String methodName) { + var possibleMethods = _instanceMethodsWithNonNullableArguments[methodName]; + if (possibleMethods == null) { + return null; + } + + if (type is! InterfaceType) { + return null; + } + + NonNullableFunction? getMethod(String library, String className) => + possibleMethods + .lookup(NonNullableFunction(library, className, methodName)); + + var element = type.element; + if (element.isSynthetic) { + return null; + } + + var method = getMethod(element.library.name, element.name); + if (method != null) { + return method; + } + + for (var supertype in element.allSupertypes) { + var superElement = supertype.element; + method = getMethod(superElement.library.name, superElement.name); + if (method != null) { + return method; + } + } + return null; + } +} diff --git a/pkg/linter/lib/src/rules/omit_local_variable_types.dart b/pkg/linter/lib/src/rules/omit_local_variable_types.dart new file mode 100644 index 00000000000..8128453b44f --- /dev/null +++ b/pkg/linter/lib/src/rules/omit_local_variable_types.dart @@ -0,0 +1,159 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Omit type annotations for local variables.'; + +const _details = r''' +**DON'T** redundantly type annotate initialized local variables. + +Local variables, especially in modern code where functions tend to be small, +have very little scope. Omitting the type focuses the reader's attention on the +more important *name* of the variable and its initialized value. + +**BAD:** +```dart +List> possibleDesserts(Set pantry) { + List> desserts = >[]; + for (final List recipe in cookbook) { + if (pantry.containsAll(recipe)) { + desserts.add(recipe); + } + } + + return desserts; +} +``` + +**GOOD:** +```dart +List> possibleDesserts(Set pantry) { + var desserts = >[]; + for (final recipe in cookbook) { + if (pantry.containsAll(recipe)) { + desserts.add(recipe); + } + } + + return desserts; +} +``` + +Sometimes the inferred type is not the type you want the variable to have. For +example, you may intend to assign values of other types later. In that case, +annotate the variable with the type you want. + +**GOOD:** +```dart +Widget build(BuildContext context) { + Widget result = Text('You won!'); + if (applyPadding) { + result = Padding(padding: EdgeInsets.all(8.0), child: result); + } + return result; +} +``` +'''; + +class OmitLocalVariableTypes extends LintRule { + static const LintCode code = LintCode('omit_local_variable_types', + 'Unnecessary type annotation on a local variable.', + correctionMessage: 'Try removing the type annotation.'); + + OmitLocalVariableTypes() + : super( + name: 'omit_local_variable_types', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['always_specify_types']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addForStatement(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitForStatement(ForStatement node) { + var loopParts = node.forLoopParts; + if (loopParts is ForPartsWithDeclarations) { + _visitVariableDeclarationList(loopParts.variables); + } else if (loopParts is ForEachPartsWithDeclaration) { + var loopVariableType = loopParts.loopVariable.type; + var staticType = loopVariableType?.type; + if (staticType == null || staticType is DynamicType) { + return; + } + var iterableType = loopParts.iterable.staticType; + if (iterableType is InterfaceType) { + // TODO(srawlins): Is `DartType.asInstanceOf` the more correct API here? + var iterableInterfaces = iterableType.implementedInterfaces + .where((type) => type.isDartCoreIterable); + if (iterableInterfaces.length == 1 && + iterableInterfaces.first.typeArguments.first == staticType) { + rule.reportLint(loopVariableType); + } + } + } + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + _visitVariableDeclarationList(node.variables); + } + + bool _dependsOnDeclaredTypeForInference(Expression? initializer) { + if (initializer is MethodInvocation) { + if (initializer.typeArguments == null) { + var element = initializer.methodName.staticElement; + if (element is FunctionElement) { + if (element.returnType is TypeParameterType) { + return true; + } + } + } + } + return false; + } + + void _visitVariableDeclarationList(VariableDeclarationList node) { + var staticType = node.type?.type; + if (staticType == null || + staticType is DynamicType || + staticType.isDartCoreNull) { + return; + } + for (var child in node.variables) { + var initializer = child.initializer; + if (initializer?.staticType != staticType) { + return; + } + if (_dependsOnDeclaredTypeForInference(initializer)) { + return; + } + } + rule.reportLint(node); + } +} diff --git a/pkg/linter/lib/src/rules/one_member_abstracts.dart b/pkg/linter/lib/src/rules/one_member_abstracts.dart new file mode 100644 index 00000000000..beb00cc4b60 --- /dev/null +++ b/pkg/linter/lib/src/rules/one_member_abstracts.dart @@ -0,0 +1,91 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Avoid defining a one-member abstract class when a simple function will do.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#avoid-defining-a-one-member-abstract-class-when-a-simple-function-will-do): + +**AVOID** defining a one-member abstract class when a simple function will do. + +Unlike Java, Dart has first-class functions, closures, and a nice light syntax +for using them. If all you need is something like a callback, just use a +function. If you're defining a class and it only has a single abstract member +with a meaningless name like `call` or `invoke`, there is a good chance +you just want a function. + +**BAD:** +```dart +abstract class Predicate { + bool test(item); +} +``` + +**GOOD:** +```dart +typedef Predicate = bool Function(item); +``` + +'''; + +class OneMemberAbstracts extends LintRule { + static const LintCode code = LintCode( + 'one_member_abstracts', 'Unnecessary use of an abstract class.', + correctionMessage: + "Try making '{0}' a top-level function and removing the class."); + + OneMemberAbstracts() + : super( + name: 'one_member_abstracts', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement == null) { + return; + } + if (declaredElement.interfaces.isNotEmpty) { + return; + } + if (declaredElement.mixins.isNotEmpty) { + return; + } + if (node.abstractKeyword != null && + node.extendsClause == null && + node.members.length == 1) { + var member = node.members.first; + if (member is MethodDeclaration && + member.isAbstract && + !member.isGetter && + !member.isSetter) { + rule.reportLintForToken(node.name, arguments: [member.name.lexeme]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/only_throw_errors.dart b/pkg/linter/lib/src/rules/only_throw_errors.dart new file mode 100644 index 00000000000..a75e7286ce3 --- /dev/null +++ b/pkg/linter/lib/src/rules/only_throw_errors.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/dart_type_utilities.dart'; + +const _desc = + r'Only throw instances of classes extending either Exception or Error.'; + +const _details = r''' +**DO** throw only instances of classes that extend `dart.core.Error` or +`dart.core.Exception`. + +Throwing instances that do not extend `Error` or `Exception` is a bad practice; +doing this is usually a hack for something that should be implemented more +thoroughly. + +**BAD:** +```dart +void throwString() { + throw 'hello world!'; // LINT +} +``` + +**GOOD:** +```dart +void throwArgumentError() { + Error error = ArgumentError('oh!'); + throw error; // OK +} +``` + +'''; + +const _errorClassName = 'Error'; + +const _exceptionClassName = 'Exception'; + +const _library = 'dart.core'; + +final LinkedHashSet _interfaceDefinitions = + LinkedHashSet.of([ + InterfaceTypeDefinition(_exceptionClassName, _library), + InterfaceTypeDefinition(_errorClassName, _library), +]); + +bool _isThrowable(DartType? type) { + var typeForInterfaceCheck = type?.typeForInterfaceCheck; + return typeForInterfaceCheck == null || + typeForInterfaceCheck is DynamicType || + type is NeverType || + typeForInterfaceCheck.implementsAnyInterface(_interfaceDefinitions); +} + +class OnlyThrowErrors extends LintRule { + static const LintCode code = LintCode( + 'only_throw_errors', + "Don't throw instances of classes that don't extend either 'Exception' " + "or 'Error'.", + correctionMessage: 'Try throwing a different class of object.'); + + OnlyThrowErrors() + : super( + name: 'only_throw_errors', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addThrowExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitThrowExpression(ThrowExpression node) { + if (node.expression is Literal) { + rule.reportLint(node.expression); + return; + } + + if (!_isThrowable(node.expression.staticType)) { + rule.reportLint(node.expression); + } + } +} diff --git a/pkg/linter/lib/src/rules/overridden_fields.dart b/pkg/linter/lib/src/rules/overridden_fields.dart new file mode 100644 index 00000000000..2fed76532c9 --- /dev/null +++ b/pkg/linter/lib/src/rules/overridden_fields.dart @@ -0,0 +1,182 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart' show IterableExtension; + +import '../analyzer.dart'; + +const _desc = r"Don't override fields."; + +const _details = r''' +**DON'T** override fields. + +Overriding fields is almost always done unintentionally. Regardless, it is a +bad practice to do so. + +**BAD:** +```dart +class Base { + Object field = 'lorem'; + + Object something = 'change'; +} + +class Bad1 extends Base { + @override + final field = 'ipsum'; // LINT +} + +class Bad2 extends Base { + @override + Object something = 'done'; // LINT +} +``` + +**GOOD:** +```dart +class Base { + Object field = 'lorem'; + + Object something = 'change'; +} + +class Ok extends Base { + Object newField; // OK + + final Object newFinal = 'ignore'; // OK +} +``` + +**GOOD:** +```dart +abstract class BaseLoggingHandler { + Base transformer; +} + +class LogPrintHandler implements BaseLoggingHandler { + @override + Derived transformer; // OK +} +``` + +'''; + +Iterable _findAllSupertypesAndMixins( + InterfaceType? interface, List accumulator) { + if (interface == null || + interface.isDartCoreObject || + accumulator.contains(interface)) { + return accumulator; + } + + accumulator.add(interface); + var superclass = interface.superclass; + var interfaces = []; + if (superclass != null) { + interfaces.add(superclass); + } + interfaces + ..addAll(interface.element.mixins) + ..addAll(_findAllSupertypesAndMixins(superclass, accumulator)); + return interfaces.where((i) => i != interface); +} + +Iterable _findAllSupertypesInMixin(MixinElement mixinElement) { + var supertypes = []; + var accumulator = []; + for (var type in mixinElement.superclassConstraints) { + supertypes.add(type); + supertypes.addAll(_findAllSupertypesAndMixins(type, accumulator)); + } + return supertypes; +} + +class OverriddenFields extends LintRule { + static const LintCode code = LintCode( + 'overridden_fields', "Field overrides a field inherited from '{0}'.", + correctionMessage: + 'Try removing the field, overriding the getter and setter if ' + 'necessary.'); + + OverriddenFields() + : super( + name: 'overridden_fields', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isStatic) { + return; + } + + for (var variable in node.fields.variables) { + var declaredField = variable.declaredElement; + if (declaredField != null) { + var overriddenField = _getOverriddenMember(declaredField); + if (overriddenField != null && !overriddenField.isAbstract) { + rule.reportLintForToken(variable.name, + arguments: [overriddenField.enclosingElement.displayName]); + } + } + } + } + + PropertyAccessorElement? _getOverriddenMember(Element member) { + var memberName = member.name; + var library = member.library; + bool isOverriddenMember(PropertyAccessorElement a) { + if (memberName == null || a.isStatic) { + return false; + } + if (a.isSynthetic && a.name == memberName) { + // Ensure that private members are overriding a member of the same library. + if (Identifier.isPrivateName(memberName)) { + return library == a.library; + } + return true; + } + return false; + } + + bool containsOverriddenMember(InterfaceType i) => + i.accessors.any(isOverriddenMember); + var enclosingElement = member.enclosingElement; + if (enclosingElement is! InterfaceElement) { + return null; + } + var classElement = enclosingElement; + + Iterable interfaces; + if (classElement is MixinElement) { + interfaces = _findAllSupertypesInMixin(classElement); + } else { + interfaces = + _findAllSupertypesAndMixins(classElement.thisType, []); + } + var interface = interfaces.firstWhereOrNull(containsOverriddenMember); + return interface?.accessors.firstWhere(isOverriddenMember); + } +} diff --git a/pkg/linter/lib/src/rules/package_api_docs.dart b/pkg/linter/lib/src/rules/package_api_docs.dart new file mode 100644 index 00000000000..18e3dc058c4 --- /dev/null +++ b/pkg/linter/lib/src/rules/package_api_docs.dart @@ -0,0 +1,142 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Provide doc comments for all public APIs.'; + +const _details = r''' +**DO** provide doc comments for all public APIs. + +As described in the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files), +public APIs consist in everything in your package's `lib` folder, minus +implementation files in `lib/src`, adding elements explicitly exported with an +`export` directive. + +For example, given `lib/foo.dart`: +```dart +export 'src/bar.dart' show Bar; +export 'src/baz.dart'; + +class Foo { } + +class _Foo { } +``` +its API includes: + +* `Foo` (but not `_Foo`) +* `Bar` (exported) and +* all *public* elements in `src/baz.dart` + +All public API members should be documented with `///` doc-style comments. + +**BAD:** +```dart +class Bar { + void bar(); +} +``` + +**GOOD:** +```dart +/// A Foo. +abstract class Foo { + /// Start foo-ing. + void start() => _start(); + + _start(); +} +``` + +Advice for writing good doc comments can be found in the +[Doc Writing Guidelines](https://dart.dev/effective-dart/documentation). + +'''; + +class PackageApiDocs extends LintRule { + static const LintCode code = LintCode( + 'package_api_docs', 'Missing documentation for public API.', + correctionMessage: 'Try adding a documentation comment.'); + + PackageApiDocs() + : super( + name: 'package_api_docs', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addClassDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addClassTypeAlias(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + } +} + +class _Visitor extends GeneralizingAstVisitor { + final PackageApiDocs rule; + + _Visitor(this.rule); + + // ignore: prefer_expression_function_bodies + void check(Declaration node) { + // See: https://github.com/dart-lang/linter/issues/3395 + // (`DartProject` removal). + return; + + // // If no project info is set, bail early. + // // https://github.com/dart-lang/linter/issues/154 + // var currentProject = rule.project; + // if (currentProject == null) { + // return; + // } + // + // var declaredElement = node.declaredElement; + // if (declaredElement != null && currentProject.isApi(declaredElement)) { + // if (node.documentationComment == null) { + // rule.reportLint(getNodeToAnnotate(node)); + // } + // } + } + + /// classMember ::= + /// [ConstructorDeclaration] + /// | [FieldDeclaration] + /// | [MethodDeclaration] + @override + void visitClassMember(ClassMember node) { + check(node); + } + + /// compilationUnitMember ::= + /// [ClassDeclaration] + /// | [EnumDeclaration] + /// | [FunctionDeclaration] + /// | [TopLevelVariableDeclaration] + /// | [ClassTypeAlias] + /// | [FunctionTypeAlias] + @override + void visitCompilationUnitMember(CompilationUnitMember node) { + check(node); + } + + @override + void visitNode(AstNode node) { + // Don't visit children + } +} diff --git a/pkg/linter/lib/src/rules/package_prefixed_library_names.dart b/pkg/linter/lib/src/rules/package_prefixed_library_names.dart new file mode 100644 index 00000000000..352c118608b --- /dev/null +++ b/pkg/linter/lib/src/rules/package_prefixed_library_names.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Prefix library names with the package name and a dot-separated path.'; + +const _details = r''' +**DO** prefix library names with the package name and a dot-separated path. + +This guideline helps avoid the warnings you get when two libraries have the same +name. Here are the rules we recommend: + +* Prefix all library names with the package name. +* Make the entry library have the same name as the package. +* For all other libraries in a package, after the package name add the +dot-separated path to the library's Dart file. +* For libraries under `lib`, omit the top directory name. + +For example, say the package name is `my_package`. Here are the library names +for various files in the package: + +**GOOD:** +```dart +// In lib/my_package.dart +library my_package; + +// In lib/other.dart +library my_package.other; + +// In lib/foo/bar.dart +library my_package.foo.bar; + +// In example/foo/bar.dart +library my_package.example.foo.bar; + +// In lib/src/private.dart +library my_package.src.private; +``` + +'''; + +/// Checks if the [name] is equivalent to the specified [prefix] or at least +/// is prefixed by it with a delimiting `.`. +bool matchesOrIsPrefixedBy(String name, String prefix) => + name == prefix || name.startsWith('$prefix.'); + +class PackagePrefixedLibraryNames extends LintRule { + static const LintCode code = LintCode( + 'package_prefixed_library_names', + 'The library name is not prefixed by the package name and a ' + 'dot-separated path.', + correctionMessage: "Try changing the name to '{0}'."); + + PackagePrefixedLibraryNames() + : super( + name: 'package_prefixed_library_names', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addLibraryDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final PackagePrefixedLibraryNames rule; + + _Visitor(this.rule); + + @override + // ignore: prefer_expression_function_bodies + void visitLibraryDirective(LibraryDirective node) { + // Project info is not being set. + //See: https://github.com/dart-lang/linter/issues/3395 + return; + + // // If no project info is set, bail early. + // // https://github.com/dart-lang/linter/issues/154 + // var project = rule.project; + // var element = node.element; + // if (project == null || element == null) { + // return; + // } + // + // var source = element.source; + // if (source == null) { + // return; + // } + // + // var prefix = Analyzer.facade.createLibraryNamePrefix( + // libraryPath: source.fullName, + // projectRoot: project.root.absolute.path, + // packageName: project.name); + // + // var name = element.name; + // if (name == null || !matchesOrIsPrefixedBy(name, prefix)) { + // rule.reportLint(node.name, arguments: ['$prefix.$name']); + // } + } +} diff --git a/pkg/linter/lib/src/rules/parameter_assignments.dart b/pkg/linter/lib/src/rules/parameter_assignments.dart new file mode 100644 index 00000000000..62d97ddc671 --- /dev/null +++ b/pkg/linter/lib/src/rules/parameter_assignments.dart @@ -0,0 +1,256 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = + r"Don't reassign references to parameters of functions or methods."; + +const _details = r''' +**DON'T** assign new values to parameters of methods or functions. + +Assigning new values to parameters is generally a bad practice unless an +operator such as `??=` is used. Otherwise, arbitrarily reassigning parameters +is usually a mistake. + +**BAD:** +```dart +void badFunction(int parameter) { // LINT + parameter = 4; +} +``` + +**BAD:** +```dart +void badFunction(int required, {int optional: 42}) { // LINT + optional ??= 8; +} +``` + +**BAD:** +```dart +void badFunctionPositional(int required, [int optional = 42]) { // LINT + optional ??= 8; +} +``` + +**BAD:** +```dart +class A { + void badMethod(int parameter) { // LINT + parameter = 4; + } +} +``` + +**GOOD:** +```dart +void ok(String parameter) { + print(parameter); +} +``` + +**GOOD:** +```dart +void actuallyGood(int required, {int optional}) { // OK + optional ??= ...; +} +``` + +**GOOD:** +```dart +void actuallyGoodPositional(int required, [int optional]) { // OK + optional ??= ...; +} +``` + +**GOOD:** +```dart +class A { + void ok(String parameter) { + print(parameter); + } +} +``` + +'''; + +bool _isDefaultFormalParameterWithDefaultValue(FormalParameter parameter) => + parameter is DefaultFormalParameter && parameter.defaultValue != null; + +bool _isFormalParameterReassigned( + FormalParameter parameter, AssignmentExpression assignment) { + var leftHandSide = assignment.leftHandSide; + return leftHandSide is SimpleIdentifier && + leftHandSide.staticElement == parameter.declaredElement; +} + +class ParameterAssignments extends LintRule { + static const LintCode code = LintCode( + 'parameter_assignments', "Invalid assignment to the parameter '{0}'.", + correctionMessage: + 'Try using a local variable in place of the parameter.'); + + ParameterAssignments() + : super( + name: 'parameter_assignments', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _DeclarationVisitor extends RecursiveAstVisitor { + final FormalParameter parameter; + final LintRule rule; + final bool paramIsNotNullByDefault; + final bool paramDefaultsToNull; + bool hasBeenAssigned = false; + + _DeclarationVisitor(this.parameter, this.rule, + {required this.paramIsNotNullByDefault, + required this.paramDefaultsToNull}); + + Element? get parameterElement => parameter.declaredElement; + + void checkPatternElements(DartPattern node) { + NodeList? fields; + if (node is RecordPattern) fields = node.fields; + if (node is ObjectPattern) fields = node.fields; + if (fields != null) { + for (var field in fields) { + if (field.pattern.element == parameterElement) { + reportLint(node); + } + } + } else { + List? elements; + if (node is ListPattern) elements = node.elements; + if (node is MapPattern) elements = node.elements; + if (elements == null) return; + for (var element in elements) { + if (element is MapPatternEntry) { + element = element.value; + } + if (element.element == parameterElement) { + reportLint(node); + } + } + } + } + + void reportLint(AstNode node) { + rule.reportLint(node, arguments: [parameter.name!.lexeme]); + } + + @override + visitAssignmentExpression(AssignmentExpression node) { + if (paramIsNotNullByDefault) { + if (_isFormalParameterReassigned(parameter, node)) { + reportLint(node); + } + } else if (paramDefaultsToNull) { + if (_isFormalParameterReassigned(parameter, node)) { + if (hasBeenAssigned) { + reportLint(node); + } + hasBeenAssigned = true; + } + } + + super.visitAssignmentExpression(node); + } + + @override + visitPatternAssignment(PatternAssignment node) { + checkPatternElements(node.pattern); + + super.visitPatternAssignment(node); + } + + @override + visitPostfixExpression(PostfixExpression node) { + if (paramIsNotNullByDefault) { + var operand = node.operand; + if (operand is SimpleIdentifier && + operand.staticElement == parameterElement) { + reportLint(node); + } + } + + super.visitPostfixExpression(node); + } + + @override + visitPrefixExpression(PrefixExpression node) { + if (paramIsNotNullByDefault) { + var operand = node.operand; + if (operand is SimpleIdentifier && + operand.staticElement == parameterElement) { + reportLint(node); + } + } + + super.visitPrefixExpression(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _checkParameters( + node.functionExpression.parameters, node.functionExpression.body); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + _checkParameters(node.parameters, node.body); + } + + void _checkParameters(FormalParameterList? parameterList, FunctionBody body) { + if (parameterList == null) return; + + for (var parameter in parameterList.parameters) { + var declaredElement = parameter.declaredElement; + if (declaredElement != null && + body.isPotentiallyMutatedInScope(declaredElement)) { + var paramIsNotNullByDefault = parameter is SimpleFormalParameter || + _isDefaultFormalParameterWithDefaultValue(parameter); + var paramDefaultsToNull = parameter is DefaultFormalParameter && + parameter.defaultValue == null; + if (paramDefaultsToNull || paramIsNotNullByDefault) { + body.accept(_DeclarationVisitor(parameter, rule, + paramDefaultsToNull: paramDefaultsToNull, + paramIsNotNullByDefault: paramIsNotNullByDefault)); + } + } + } + } +} + +extension on AstNode { + Element? get element { + var self = this; + if (self is AssignedVariablePattern) return self.element; + return null; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_adjacent_string_concatenation.dart b/pkg/linter/lib/src/rules/prefer_adjacent_string_concatenation.dart new file mode 100644 index 00000000000..a0efad743b5 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_adjacent_string_concatenation.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use adjacent strings to concatenate string literals.'; + +const _details = r''' +**DO** use adjacent strings to concatenate string literals. + +**BAD:** +```dart +raiseAlarm( + 'ERROR: Parts of the spaceship are on fire. Other ' + + 'parts are overrun by martians. Unclear which are which.'); +``` + +**GOOD:** +```dart +raiseAlarm( + 'ERROR: Parts of the spaceship are on fire. Other ' + 'parts are overrun by martians. Unclear which are which.'); +``` + +'''; + +class PreferAdjacentStringConcatenation extends LintRule { + static const LintCode code = LintCode('prefer_adjacent_string_concatenation', + "String literals shouldn't be concatenated by the '+' operator.", + correctionMessage: 'Try removing the operator to use adjacent strings.'); + + PreferAdjacentStringConcatenation() + : super( + name: 'prefer_adjacent_string_concatenation', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBinaryExpression(BinaryExpression node) { + if (node.operator.type.lexeme == '+' && + node.leftOperand is StringLiteral && + node.rightOperand is StringLiteral) { + rule.reportLintForToken(node.operator); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_asserts_in_initializer_lists.dart b/pkg/linter/lib/src/rules/prefer_asserts_in_initializer_lists.dart new file mode 100644 index 00000000000..bf388c3d405 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_asserts_in_initializer_lists.dart @@ -0,0 +1,173 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Prefer putting asserts in initializer lists.'; + +const _details = r''' +**DO** put asserts in initializer lists. + +**BAD:** +```dart +class A { + A(int a) { + assert(a != 0); + } +} +``` + +**GOOD:** +```dart +class A { + A(int a) : assert(a != 0); +} +``` + +'''; + +class PreferAssertsInInitializerLists extends LintRule { + static const LintCode code = LintCode('prefer_asserts_in_initializer_lists', + 'Assert should be in the initializer list.', + correctionMessage: 'Try moving the assert to the initializer list.'); + + PreferAssertsInInitializerLists() + : super( + name: 'prefer_asserts_in_initializer_lists', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _AssertVisitor extends RecursiveAstVisitor { + final ConstructorElement constructorElement; + final _ClassAndSuperClasses? classAndSuperClasses; + + bool needInstance = false; + + _AssertVisitor(this.constructorElement, this.classAndSuperClasses); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + var element = getWriteOrReadElement(node); + + // use method + needInstance = needInstance || + element is MethodElement && !element.isStatic && _hasMethod(element); + + // use property accessor not used as field formal parameter + needInstance = needInstance || + element is PropertyAccessorElement && + !element.isStatic && + _hasAccessor(element) && + !_paramMatchesField(element, constructorElement.parameters); + } + + @override + void visitThisExpression(ThisExpression node) { + needInstance = true; + } + + bool _hasAccessor(PropertyAccessorElement element) { + var classes = classAndSuperClasses?.classes; + return classes != null && classes.contains(element.enclosingElement); + } + + bool _hasMethod(MethodElement element) { + var classes = classAndSuperClasses?.classes; + return classes != null && classes.contains(element.enclosingElement); + } + + bool _paramMatchesField( + PropertyAccessorElement element, List parameters) { + for (var p in parameters) { + ParameterElement? parameterElement = p; + if (parameterElement is SuperFormalParameterElement) { + parameterElement = parameterElement.superConstructorParameter; + } + + if (parameterElement is FieldFormalParameterElement) { + if (parameterElement.field?.getter == element) { + return true; + } + } + } + return false; + } +} + +/// Lazy cache of elements. +class _ClassAndSuperClasses { + final ClassElement? element; + final Set _classes = {}; + + _ClassAndSuperClasses(this.element); + + /// The [element] and its super classes, including mixins. + Set get classes { + if (_classes.isEmpty) { + void addRecursively(InterfaceElement? element) { + if (element != null && _classes.add(element)) { + for (var t in element.mixins) { + addRecursively(t.element); + } + addRecursively(element.supertype?.element); + } + } + + addRecursively(element); + } + + return _classes; + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _ClassAndSuperClasses? _classAndSuperClasses; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + _classAndSuperClasses = _ClassAndSuperClasses(node.declaredElement); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement == null || declaredElement.isFactory) return; + + var body = node.body; + if (body is BlockFunctionBody) { + for (var statement in body.block.statements) { + if (statement is! AssertStatement) break; + + var assertVisitor = + _AssertVisitor(declaredElement, _classAndSuperClasses); + statement.visitChildren(assertVisitor); + if (!assertVisitor.needInstance) { + rule.reportLintForToken(statement.beginToken); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_asserts_with_message.dart b/pkg/linter/lib/src/rules/prefer_asserts_with_message.dart new file mode 100644 index 00000000000..ed67acf0cc5 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_asserts_with_message.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer asserts with message.'; + +const _details = r''' +When assertions fail it's not always simple to understand why. Adding a message +to the `assert` helps the developer to understand why the AssertionError occurs. + +**BAD:** +```dart +f(a) { + assert(a != null); +} + +class A { + A(a) : assert(a != null); +} +``` + +**GOOD:** +```dart +f(a) { + assert(a != null, 'a must not be null'); +} + +class A { + A(a) : assert(a != null, 'a must not be null'); +} +``` + +'''; + +class PreferAssertsWithMessage extends LintRule { + static const LintCode code = LintCode( + 'prefer_asserts_with_message', 'Missing a message in an assert.', + correctionMessage: 'Try adding a message to the assert.'); + + PreferAssertsWithMessage() + : super( + name: 'prefer_asserts_with_message', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAssertInitializer(this, visitor); + registry.addAssertStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAssertInitializer(AssertInitializer node) { + if (node.message == null) { + rule.reportLint(node); + } + } + + @override + void visitAssertStatement(AssertStatement node) { + if (node.message == null) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_bool_in_asserts.dart b/pkg/linter/lib/src/rules/prefer_bool_in_asserts.dart new file mode 100644 index 00000000000..21bd126c5be --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_bool_in_asserts.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = r'Prefer using a boolean as the assert condition.'; + +const _details = r''' +**DO** use a boolean for assert conditions. + +Not using booleans in assert conditions can lead to code where it isn't clear +what the intention of the assert statement is. + +**BAD:** +```dart +assert(() { + f(); + return true; +}); +``` + +**GOOD:** +```dart +assert(() { + f(); + return true; +}()); +``` +'''; + +class PreferBoolInAsserts extends LintRule { + static const LintCode code = LintCode('prefer_bool_in_asserts', + "Use an expression that returns a 'bool' as the 'assert' condition.", + correctionMessage: + "Try rewriting the 'assert' condition to return a 'bool'."); + + PreferBoolInAsserts() + : super( + name: 'prefer_bool_in_asserts', + description: _desc, + details: _details, + state: State.removed(since: dart3), + group: Group.style); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/prefer_collection_literals.dart b/pkg/linter/lib/src/rules/prefer_collection_literals.dart new file mode 100644 index 00000000000..8533ea35f78 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_collection_literals.dart @@ -0,0 +1,194 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use collection literals when possible.'; + +const _details = r''' +**DO** use collection literals when possible. + +**BAD:** +```dart +var addresses = Map(); +var uniqueNames = Set(); +var ids = LinkedHashSet(); +var coordinates = LinkedHashMap(); +``` + +**GOOD:** +```dart +var addresses = {}; +var uniqueNames = {}; +var ids = {}; +var coordinates = {}; +``` + +**EXCEPTIONS:** + +There are cases with `LinkedHashSet` or `LinkedHashMap` where a literal constructor +will trigger a type error so those will be excluded from the lint. + +```dart +void main() { + LinkedHashSet linkedHashSet = LinkedHashSet.from([1, 2, 3]); // OK + LinkedHashMap linkedHashMap = LinkedHashMap(); // OK + + printSet(LinkedHashSet()); // LINT + printHashSet(LinkedHashSet()); // OK + + printMap(LinkedHashMap()); // LINT + printHashMap(LinkedHashMap()); // OK +} + +void printSet(Set ids) => print('$ids!'); +void printHashSet(LinkedHashSet ids) => printSet(ids); +void printMap(Map map) => print('$map!'); +void printHashMap(LinkedHashMap map) => printMap(map); +``` +'''; + +class PreferCollectionLiterals extends LintRule { + static const LintCode code = LintCode( + 'prefer_collection_literals', 'Unnecessary constructor invocation.', + correctionMessage: 'Try using a collection literal.'); + + PreferCollectionLiterals() + : super( + name: 'prefer_collection_literals', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var constructorName = node.constructorName.name?.name; + + // Maps. + if (_isMap(node) || _isHashMap(node)) { + if (_shouldSkipLinkedHashLint(node, _isTypeHashMap)) { + return; + } + if (constructorName == null && node.argumentList.arguments.isEmpty) { + rule.reportLint(node); + } + return; + } + + // Sets. + if (_isSet(node) || _isHashSet(node)) { + if (_shouldSkipLinkedHashLint(node, _isTypeHashSet)) { + return; + } + + var args = node.argumentList.arguments; + if (constructorName == null) { + // Skip: LinkedHashSet(equals: (a, b) => false, hashCode: (o) => 13) + if (args.isEmpty) { + rule.reportLint(node); + } + } else if (constructorName == 'from' || constructorName == 'of') { + if (args.length != 1) { + return; + } + if (args.first is ListLiteral) { + rule.reportLint(node); + } + } + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + // ['foo', 'bar', 'baz'].toSet(); + if (node.methodName.name != 'toSet') { + return; + } + if (node.target is ListLiteral) { + rule.reportLint(node); + } + } + + bool _isHashMap(Expression expression) => + _isTypeHashMap(expression.staticType); + + bool _isHashSet(Expression expression) => + _isTypeHashSet(expression.staticType); + + bool _isMap(Expression expression) => + expression.staticType?.isDartCoreMap ?? false; + + bool _isSet(Expression expression) => + expression.staticType?.isDartCoreSet ?? false; + + bool _isTypeHashMap(DartType? type) => + type.isSameAs('LinkedHashMap', 'dart.collection'); + + bool _isTypeHashSet(DartType? type) => + type.isSameAs('LinkedHashSet', 'dart.collection'); + + bool _shouldSkipLinkedHashLint( + InstanceCreationExpression node, bool Function(DartType node) typeCheck) { + if (_isHashMap(node) || _isHashSet(node)) { + // Skip: LinkedHashSet s = ...; or LinkedHashMap s = ...; + var parent = node.parent; + if (parent is VariableDeclaration) { + var parent2 = parent.parent; + if (parent2 is VariableDeclarationList) { + var assignmentType = parent2.type?.type; + if (assignmentType != null && typeCheck(assignmentType)) { + return true; + } + } + } + // Skip: function(LinkedHashSet()); when function(LinkedHashSet mySet) or + // function(LinkedHashMap()); when function(LinkedHashMap myMap) + if (parent is ArgumentList) { + var paramType = node.staticParameterElement?.type; + if (paramType == null || typeCheck(paramType)) { + return true; + } + } + + // Skip: void f({required LinkedHashSet s}) + if (parent is NamedExpression) { + var paramType = parent.staticParameterElement?.type; + if (paramType != null && typeCheck(paramType)) { + return true; + } + } + + // Skip: {}.putIfAbsent(3, () => LinkedHashSet()); + // or {}.putIfAbsent(3, () => LinkedHashMap()); + if (parent is ExpressionFunctionBody) { + var expressionType = parent.expression.staticType; + if (expressionType != null && typeCheck(expressionType)) { + return true; + } + } + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_conditional_assignment.dart b/pkg/linter/lib/src/rules/prefer_conditional_assignment.dart new file mode 100644 index 00000000000..2ce482f7e66 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_conditional_assignment.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/dart_type_utilities.dart' as type_utils; + +const _desc = r'Prefer using `??=` over testing for null.'; + +const _details = r''' +**PREFER** using `??=` over testing for null. + +As Dart has the `??=` operator, it is advisable to use it where applicable to +improve the brevity of your code. + +**BAD:** +```dart +String get fullName { + if (_fullName == null) { + _fullName = getFullUserName(this); + } + return _fullName; +} +``` + +**GOOD:** +```dart +String get fullName { + return _fullName ??= getFullUserName(this); +} +``` + +'''; + +bool _checkExpression(Expression expression, Expression condition) => + expression is AssignmentExpression && + type_utils.canonicalElementsFromIdentifiersAreEqual( + expression.leftHandSide, condition); + +bool _checkStatement(Statement statement, Expression condition) { + if (statement is ExpressionStatement) { + return _checkExpression(statement.expression, condition); + } + if (statement is Block && statement.statements.length == 1) { + return _checkStatement(statement.statements.first, condition); + } + return false; +} + +Expression? _getExpressionCondition(Expression rawExpression) { + var expression = rawExpression.unParenthesized; + if (expression is BinaryExpression && + expression.operator.type == TokenType.EQ_EQ) { + if (expression.rightOperand.isNullLiteral) { + return expression.leftOperand; + } + if (expression.leftOperand.isNullLiteral) { + return expression.rightOperand; + } + } + return null; +} + +class PreferConditionalAssignment extends LintRule { + static const LintCode code = LintCode('prefer_conditional_assignment', + "The 'if' statement could be replaced by a null-aware assignment.", + correctionMessage: + "Try using the '??=' operator to conditionally assign a value."); + + PreferConditionalAssignment() + : super( + name: 'prefer_conditional_assignment', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addIfStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitIfStatement(IfStatement node) { + if (node.elseStatement != null) { + return; + } + var expressionInCondition = _getExpressionCondition(node.expression); + if (expressionInCondition != null && + _checkStatement(node.thenStatement, expressionInCondition)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_const_constructors.dart b/pkg/linter/lib/src/rules/prefer_const_constructors.dart new file mode 100644 index 00000000000..9238634b837 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_const_constructors.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer const with constant constructors.'; + +const _details = r''' +**PREFER** using `const` for instantiating constant constructors. + +If a constructor can be invoked as const to produce a canonicalized instance, +it's preferable to do so. + +**BAD:** +```dart +class A { + const A(); +} + +void accessA() { + A a = new A(); +} +``` + +**GOOD:** +```dart +class A { + const A(); +} + +void accessA() { + A a = const A(); +} +``` + +**GOOD:** +```dart +class A { + final int x; + + const A(this.x); +} + +A foo(int x) => new A(x); +``` + +'''; + +class PreferConstConstructors extends LintRule { + static const LintCode code = LintCode('prefer_const_constructors', + "Use 'const' with the constructor to improve performance.", + correctionMessage: + "Try adding the 'const' keyword to the constructor invocation."); + + PreferConstConstructors() + : super( + name: 'prefer_const_constructors', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.constructorName.type.isDeferred) return; + + var element = node.constructorName.staticElement; + if (element == null) return; + + if (element.isConst && !node.isConst) { + // Handled by analyzer hint. + if (element.hasLiteral) return; + + var enclosingElement = element.enclosingElement; + if (enclosingElement is ClassElement && + enclosingElement.isDartCoreObject) { + // Skip lint for `new Object()`, because it can be used for Id creation. + return; + } + + if (context.canBeConst(node)) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_const_constructors_in_immutables.dart b/pkg/linter/lib/src/rules/prefer_const_constructors_in_immutables.dart new file mode 100644 index 00000000000..a44209ce784 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_const_constructors_in_immutables.dart @@ -0,0 +1,145 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart' show IterableExtension; + +import '../analyzer.dart'; + +const _desc = r'Prefer declaring const constructors on `@immutable` classes.'; + +const _details = r''' +**PREFER** declaring const constructors on `@immutable` classes. + +If a class is immutable, it is usually a good idea to make its constructor a +const constructor. + +**BAD:** +```dart +@immutable +class A { + final a; + A(this.a); +} +``` + +**GOOD:** +```dart +@immutable +class A { + final a; + const A(this.a); +} +``` + +'''; + +/// The name of the top-level variable used to mark a immutable class. +String _immutableVarName = 'immutable'; + +/// The name of `meta` library, used to define analysis annotations. +String _metaLibName = 'meta'; + +bool _isImmutable(Element? element) => + element is PropertyAccessorElement && + element.name == _immutableVarName && + element.library.name == _metaLibName; + +class PreferConstConstructorsInImmutables extends LintRule { + static const LintCode code = LintCode( + 'prefer_const_constructors_in_immutables', + "Constructors in '@immutable' classes should be declared as 'const'.", + correctionMessage: "Try adding 'const' to the constructor declaration."); + + PreferConstConstructorsInImmutables() + : super( + name: 'prefer_const_constructors_in_immutables', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var element = node.declaredElement; + if (element == null) return; + if (element.isConst) return; + if (node.body is! EmptyFunctionBody) return; + if (element.enclosingElement.mixins.isNotEmpty) return; + if (!_hasImmutableAnnotation(element.enclosingElement)) return; + var isRedirected = + element.isFactory && element.redirectedConstructor != null; + if (isRedirected && (element.redirectedConstructor?.isConst ?? false)) { + rule.reportLintForToken(node.firstTokenAfterCommentAndMetadata); + } + if (!isRedirected && + _hasConstConstructorInvocation(node) && + context.canBeConstConstructor(node)) { + rule.reportLintForToken(node.firstTokenAfterCommentAndMetadata); + } + } + + bool _hasConstConstructorInvocation(ConstructorDeclaration node) { + var declaredElement = node.declaredElement; + if (declaredElement == null) { + return false; + } + var clazz = declaredElement.enclosingElement; + // Constructor with super-initializer. + var superInvocation = + node.initializers.whereType().firstOrNull; + if (superInvocation != null) { + return superInvocation.staticElement?.isConst ?? false; + } + // Constructor with 'this' redirecting initializer. + var redirectInvocation = node.initializers + .whereType() + .firstOrNull; + if (redirectInvocation != null) { + return redirectInvocation.staticElement?.isConst ?? false; + } + // Constructor with implicit `super()` call. + var unnamedSuperConstructor = + clazz.supertype?.constructors.firstWhereOrNull((e) => e.name.isEmpty); + return unnamedSuperConstructor != null && unnamedSuperConstructor.isConst; + } + + /// Whether [clazz] or any of it's super-types are annotated with + /// `@immutable`. + bool _hasImmutableAnnotation(InterfaceElement clazz) { + var selfAndInheritedClasses = _getSelfAndSuperClasses(clazz); + for (var cls in selfAndInheritedClasses) { + if (cls.metadata.any((m) => _isImmutable(m.element))) return true; + } + return false; + } + + static List _getSelfAndSuperClasses(InterfaceElement self) { + InterfaceElement? current = self; + var seenElements = {}; + while (current != null && seenElements.add(current)) { + current = current.supertype?.element; + } + return seenElements.toList(); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_const_declarations.dart b/pkg/linter/lib/src/rules/prefer_const_declarations.dart new file mode 100644 index 00000000000..fa2fc346304 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_const_declarations.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Prefer const over final for declarations.'; + +const _details = r''' +**PREFER** using `const` for const declarations. + +Const declarations are more hot-reload friendly and allow to use const +constructors if an instantiation references this declaration. + +**BAD:** +```dart +final o = const []; + +class A { + static final o = const []; +} +``` + +**GOOD:** +```dart +const o = []; + +class A { + static const o = []; +} +``` + +'''; + +class PreferConstDeclarations extends LintRule { + static const LintCode code = LintCode('prefer_const_declarations', + "Use 'const' for final variables initialized to a constant value.", + correctionMessage: "Try replacing 'final' with 'const'."); + + PreferConstDeclarations() + : super( + name: 'prefer_const_declarations', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addFieldDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (!node.isStatic) return; + _visitVariableDeclarationList(node.fields); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) => + _visitVariableDeclarationList(node.variables); + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) => + _visitVariableDeclarationList(node.variables); + + void _visitVariableDeclarationList(VariableDeclarationList node) { + if (node.isConst) return; + if (!node.isFinal) return; + if (node.variables.every((declaration) { + var initializer = declaration.initializer; + return initializer != null && + (initializer is! TypedLiteral || + (initializer.beginToken.keyword == Keyword.CONST)) && + !hasConstantError(context, initializer); + })) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_const_literals_to_create_immutables.dart b/pkg/linter/lib/src/rules/prefer_const_literals_to_create_immutables.dart new file mode 100644 index 00000000000..cf0a31d047b --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_const_literals_to_create_immutables.dart @@ -0,0 +1,133 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const desc = + 'Prefer const literals as parameters of constructors on @immutable classes.'; + +const details = ''' +**PREFER** using `const` for instantiating list, map and set literals used as +parameters in immutable class instantiations. + +**BAD:** +```dart +@immutable +class A { + A(this.v); + final v; +} + +A a1 = new A([1]); +A a2 = new A({}); +``` + +**GOOD:** +```dart +A a1 = new A(const [1]); +A a2 = new A(const {}); +``` + +'''; + +/// The name of the top-level variable used to mark a immutable class. +String _immutableVarName = 'immutable'; + +/// The name of `meta` library, used to define analysis annotations. +String _metaLibName = 'meta'; + +bool _isImmutable(Element? element) => + element is PropertyAccessorElement && + element.name == _immutableVarName && + element.library.name == _metaLibName; + +class PreferConstLiteralsToCreateImmutables extends LintRule { + static const LintCode code = LintCode( + 'prefer_const_literals_to_create_immutables', + "Use 'const' literals as arguments to constructors of '@immutable' " + 'classes.', + correctionMessage: "Try adding 'const' before the literal."); + + PreferConstLiteralsToCreateImmutables() + : super( + name: 'prefer_const_literals_to_create_immutables', + description: desc, + details: details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addListLiteral(this, visitor); + registry.addSetOrMapLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitListLiteral(ListLiteral node) => _visitTypedLiteral(node); + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + _visitTypedLiteral(node); + } + + void _visitTypedLiteral(TypedLiteral literal) { + if (literal.isConst) return; + + // looking for parent instance creation to check if class is immutable + AstNode? node = literal; + while (node is! InstanceCreationExpression && + (node is ParenthesizedExpression || + node is ArgumentList || + node is ListLiteral || + node is SetOrMapLiteral || + node is MapLiteralEntry || + node is NamedExpression)) { + node = node?.parent; + } + if (!(node is InstanceCreationExpression && + _hasImmutableAnnotation(node.staticType))) { + return; + } + + if (context.canBeConst(literal)) { + rule.reportLint(literal); + } + } + + // todo(pq): consider making this a utility and sharing w/ `avoid_equals_and_hash_code_on_mutable_classes` + static bool _hasImmutableAnnotation(DartType? type) { + if (type is! InterfaceType) { + // This happens when we find an instance creation expression for a class + // that cannot be resolved. + return false; + } + + InterfaceType? current = type; + while (current != null) { + for (var annotation in current.element.metadata) { + if (_isImmutable(annotation.element)) return true; + } + current = current.superclass; + } + + return false; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_constructors_over_static_methods.dart b/pkg/linter/lib/src/rules/prefer_constructors_over_static_methods.dart new file mode 100644 index 00000000000..61ebb5edad4 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_constructors_over_static_methods.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Prefer defining constructors instead of static methods to create instances.'; + +const _details = r''' +**PREFER** defining constructors instead of static methods to create instances. + +In most cases, it makes more sense to use a named constructor rather than a +static method because it makes instantiation clearer. + +**BAD:** +```dart +class Point { + num x, y; + Point(this.x, this.y); + static Point polar(num theta, num radius) { + return Point(radius * math.cos(theta), + radius * math.sin(theta)); + } +} +``` + +**GOOD:** +```dart +class Point { + num x, y; + Point(this.x, this.y); + Point.polar(num theta, num radius) + : x = radius * math.cos(theta), + y = radius * math.sin(theta); +} +``` +'''; + +// todo(pq): temporary; remove after renamed class is in the SDK +// ignore: non_constant_identifier_names +LintRule PreferConstructorsInsteadOfStaticMethods() => + PreferConstructorsOverStaticMethods(); + +bool _hasNewInvocation(DartType returnType, FunctionBody body) => + _BodyVisitor(returnType).containsInstanceCreation(body); + +class PreferConstructorsOverStaticMethods extends LintRule { + static const LintCode code = LintCode( + 'prefer_constructors_over_static_methods', + 'Static method should be a constructor.', + correctionMessage: 'Try converting the method into a constructor.'); + + PreferConstructorsOverStaticMethods() + : super( + name: 'prefer_constructors_over_static_methods', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addMethodDeclaration(this, visitor); + } +} + +class _BodyVisitor extends RecursiveAstVisitor { + bool found = false; + + final DartType returnType; + _BodyVisitor(this.returnType); + + bool containsInstanceCreation(FunctionBody body) { + body.accept(this); + return found; + } + + @override + visitInstanceCreationExpression(InstanceCreationExpression node) { + found = node.staticType == returnType; + if (!found) { + super.visitInstanceCreationExpression(node); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (!node.isStatic) return; + if (node.typeParameters != null) return; + var returnType = node.returnType?.type; + if (returnType is! InterfaceType) return; + + var interfaceType = node.parent.typeToCheckOrNull(); + if (interfaceType != null) { + if (!context.typeSystem.isAssignableTo(returnType, interfaceType)) { + return; + } + if (_hasNewInvocation(returnType, node.body)) { + rule.reportLintForToken(node.name); + } + } + } +} + +extension on AstNode? { + InterfaceType? typeToCheckOrNull() => switch (this) { + ExtensionTypeDeclaration e => + e.typeParameters == null ? e.declaredElement?.thisType : null, + ClassDeclaration c => + c.typeParameters == null ? c.declaredElement?.thisType : null, + _ => null + }; +} diff --git a/pkg/linter/lib/src/rules/prefer_contains.dart b/pkg/linter/lib/src/rules/prefer_contains.dart new file mode 100644 index 00000000000..fb535acccfa --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_contains.dart @@ -0,0 +1,171 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; +import '../util/dart_type_utilities.dart'; + +const _desc = r'Use contains for `List` and `String` instances.'; +const _details = r''' +**DON'T** use `indexOf` to see if a collection contains an element. + +Calling `indexOf` to see if a collection contains something is difficult to read +and may have poor performance. + +Instead, prefer `contains`. + +**BAD:** +```dart +if (lunchBox.indexOf('sandwich') == -1) return 'so hungry...'; +``` + +**GOOD:** +```dart +if (!lunchBox.contains('sandwich')) return 'so hungry...'; +``` + +'''; + +class PreferContains extends LintRule { + // TODO(brianwilkerson) Both `alwaysFalse` and `alwaysTrue` should be warnings + // rather than lints because they represent a bug rather than a style + // preference. + static const LintCode alwaysFalse = LintCode('prefer_contains', + 'Always false because indexOf is always greater or equal -1.'); + + static const LintCode alwaysTrue = LintCode('prefer_contains', + 'Always true because indexOf is always greater or equal -1.'); + + static const LintCode useContains = LintCode('prefer_contains', + "Unnecessary use of 'indexOf' to test for containment.", + correctionMessage: "Try using 'contains'."); + + PreferContains() + : super( + name: 'prefer_contains', + description: _desc, + details: _details, + group: Group.style); + + @override + List get lintCodes => [alwaysFalse, alwaysTrue, useContains]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final PreferContains rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitBinaryExpression(BinaryExpression node) { + // This lint rule is only concerned with these operators. + if (!node.operator.matchesAny([ + TokenType.EQ_EQ, + TokenType.BANG_EQ, + TokenType.GT, + TokenType.GT_EQ, + TokenType.LT, + TokenType.LT_EQ, + ])) { + return; + } + var value = getIntValue(node.rightOperand, context); + if (value is int) { + if (value <= 0 && _isUnassignedIndexOf(node.leftOperand)) { + _checkConstant(node, value, node.operator.type); + } + } else { + value = getIntValue(node.leftOperand, context); + if (value is int) { + if (value <= 0 && _isUnassignedIndexOf(node.rightOperand)) { + _checkConstant(node, value, node.operator.type.inverted); + } + } + } + } + + void _checkConstant(Expression expression, int value, TokenType type) { + if (value == -1) { + if (type == TokenType.EQ_EQ || + type == TokenType.BANG_EQ || + type == TokenType.LT_EQ || + type == TokenType.GT) { + rule.reportLint(expression, errorCode: PreferContains.useContains); + } else if (type == TokenType.LT) { + // indexOf < -1 is always false + rule.reportLint(expression, errorCode: PreferContains.alwaysFalse); + } else if (type == TokenType.GT_EQ) { + // indexOf >= -1 is always true + rule.reportLint(expression, errorCode: PreferContains.alwaysTrue); + } + } else if (value == 0) { + // 'indexOf >= 0' is same as 'contains', + // and 'indexOf < 0' is same as '!contains' + if (type == TokenType.GT_EQ || type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferContains.useContains); + } + } else if (value < -1) { + // 'indexOf' is always >= -1, so comparing with lesser values makes + // no sense. + if (type == TokenType.EQ_EQ || + type == TokenType.LT_EQ || + type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferContains.alwaysFalse); + } else if (type == TokenType.BANG_EQ || + type == TokenType.GT_EQ || + type == TokenType.GT) { + rule.reportLint(expression, errorCode: PreferContains.alwaysTrue); + } + } + } + + /// Returns whether [expression] is an invocation of `Iterable.indexOf` or + /// `String.indexOf`, which is not assigned to a value. + bool _isUnassignedIndexOf(Expression expression) { + // Unwrap parens and `as` expressions. + var invocation = expression.unParenthesized; + while (invocation is AsExpression) { + invocation = invocation.expression; + } + invocation = invocation.unParenthesized; + + if (invocation is! MethodInvocation) return false; + + // The result of `indexOf` is being assigned before being compared, so + // it's important. E.g. `(next = list.indexOf('{')) != -1)`. + if (invocation.parent is AssignmentExpression) return false; + if (invocation.methodName.name != 'indexOf') return false; + + var parentType = invocation.target?.staticType; + if (parentType == null) return false; + if (!parentType.implementsAnyInterface([ + InterfaceTypeDefinition('Iterable', 'dart.core'), + InterfaceTypeDefinition('String', 'dart.core'), + ])) { + return false; + } + + var args = invocation.argumentList.arguments; + if (args.length == 2) { + var start = args[1]; + if (getIntValue(start, context) != 0) return false; + } + + return true; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_double_quotes.dart b/pkg/linter/lib/src/rules/prefer_double_quotes.dart new file mode 100644 index 00000000000..8d4db54d618 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_double_quotes.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; +import '../rules/prefer_single_quotes.dart'; + +const _desc = + r"Prefer double quotes where they won't require escape sequences."; + +const _details = ''' +**DO** use double quotes where they wouldn't require additional escapes. + +That means strings with a double quote may use apostrophes so that the double +quote isn't escaped (note: we don't lint the other way around, ie, a double +quoted string with an escaped double quote is not flagged). + +It's also rare, but possible, to have strings within string interpolations. In +this case, its much more readable to use a single quote somewhere. So single +quotes are allowed either within, or containing, an interpolated string literal. +Arguably strings within string interpolations should be its own type of lint. + +**BAD:** +```dart +useStrings( + 'should be double quote', + r'should be double quote', + r\'''should be double quotes\''') +``` + +**GOOD:** +```dart +useStrings( + "should be double quote", + r"should be double quote", + r"""should be double quotes""", + 'ok with " inside', + 'nested \${a ? "strings" : "can"} be wrapped by a double quote', + "and nested \${a ? 'strings' : 'can be double quoted themselves'}"); +``` + +'''; + +class PreferDoubleQuotes extends LintRule { + static const LintCode code = LintCode( + 'prefer_double_quotes', 'Unnecessary use of single quotes.', + correctionMessage: + 'Try using double quotes unless the string contains double quotes.'); + + PreferDoubleQuotes() + : super( + name: 'prefer_double_quotes', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['prefer_single_quotes']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = QuoteVisitor(this, useSingle: false); + registry.addSimpleStringLiteral(this, visitor); + registry.addStringInterpolation(this, visitor); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_equal_for_default_values.dart b/pkg/linter/lib/src/rules/prefer_equal_for_default_values.dart new file mode 100644 index 00000000000..88b34cf7fc6 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_equal_for_default_values.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = r'Use `=` to separate a named parameter from its default value.'; + +const _details = r''' +**DO** use `=` to separate a named parameter from its default value. + +**BAD:** +```dart +m({a: 1}) +``` + +**GOOD:** +```dart +m({a = 1}) +``` +'''; + +class PreferEqualForDefaultValues extends LintRule { + static const LintCode code = LintCode('prefer_equal_for_default_values', + "Default values should be introduced by '=' rather than ':'.", + correctionMessage: "Try using '=' to introduce the default value."); + + PreferEqualForDefaultValues() + : super( + name: 'prefer_equal_for_default_values', + description: _desc, + details: _details, + state: State.removed(since: dart3), + group: Group.style); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/prefer_expression_function_bodies.dart b/pkg/linter/lib/src/rules/prefer_expression_function_bodies.dart new file mode 100644 index 00000000000..c742fab1d0a --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_expression_function_bodies.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Use => for short members whose body is a single return statement.'; + +const _details = r''' +**CONSIDER** using => for short members whose body is a single return statement. + +**BAD:** +```dart +get width { + return right - left; +} +``` + +**BAD:** +```dart +bool ready(num time) { + return minTime == null || minTime <= time; +} +``` + +**BAD:** +```dart +containsValue(String value) { + return getValues().contains(value); +} +``` + +**GOOD:** +```dart +get width => right - left; +``` + +**GOOD:** +```dart +bool ready(num time) => minTime == null || minTime <= time; +``` + +**GOOD:** +```dart +containsValue(String value) => getValues().contains(value); +``` + +'''; + +class PreferExpressionFunctionBodies extends LintRule { + static const LintCode code = LintCode('prefer_expression_function_bodies', + 'Unnecessary use of a block function body.', + correctionMessage: 'Try using an expression function body.'); + + PreferExpressionFunctionBodies() + : super( + name: 'prefer_expression_function_bodies', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBlockFunctionBody(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + var statements = node.block.statements; + if (statements.length != 1) return; + + var uniqueStatement = node.block.statements.single; + if (uniqueStatement is! ReturnStatement) return; + if (uniqueStatement.expression == null) return; + + rule.reportLint(node); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_final_fields.dart b/pkg/linter/lib/src/rules/prefer_final_fields.dart new file mode 100644 index 00000000000..fc86e965d19 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_final_fields.dart @@ -0,0 +1,239 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Private field could be final.'; + +const _details = r''' +**DO** prefer declaring private fields as final if they are not reassigned later +in the library. + +Declaring fields as final when possible is a good practice because it helps +avoid accidental reassignments and allows the compiler to do optimizations. + +**BAD:** +```dart +class BadImmutable { + var _label = 'hola mundo! BadImmutable'; // LINT + var label = 'hola mundo! BadImmutable'; // OK +} +``` + +**BAD:** +```dart +class MultipleMutable { + var _label = 'hola mundo! GoodMutable', _offender = 'mumble mumble!'; // LINT + var _someOther; // LINT + + MultipleMutable() : _someOther = 5; + + MultipleMutable(this._someOther); + + void changeLabel() { + _label= 'hello world! GoodMutable'; + } +} +``` + +**GOOD:** +```dart +class GoodImmutable { + final label = 'hola mundo! BadImmutable', bla = 5; // OK + final _label = 'hola mundo! BadImmutable', _bla = 5; // OK +} +``` + +**GOOD:** +```dart +class GoodMutable { + var _label = 'hola mundo! GoodMutable'; + + void changeLabel() { + _label = 'hello world! GoodMutable'; + } +} +``` + +**BAD:** +```dart +class AssignedInAllConstructors { + var _label; // LINT + AssignedInAllConstructors(this._label); + AssignedInAllConstructors.withDefault() : _label = 'Hello'; +} +``` + +**GOOD:** +```dart +class NotAssignedInAllConstructors { + var _label; // OK + NotAssignedInAllConstructors(); + NotAssignedInAllConstructors.withDefault() : _label = 'Hello'; +} +``` +'''; + +class PreferFinalFields extends LintRule { + static const LintCode code = LintCode( + 'prefer_final_fields', "The private field {0} could be 'final'.", + correctionMessage: "Try making the field 'final'."); + + PreferFinalFields() + : super( + name: 'prefer_final_fields', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _DeclarationsCollector extends RecursiveAstVisitor { + final fields = {}; + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isInvalidExtensionTypeField) return; + if (node.parent is EnumDeclaration) return; + if (node.fields.isFinal || node.fields.isConst) { + return; + } + + for (var variable in node.fields.variables) { + var element = variable.declaredElement; + if (element is FieldElement && + element.isPrivate && + !element.overridesField) { + fields[element] = variable; + } + } + } +} + +class _FieldMutationFinder extends RecursiveAstVisitor { + /// The collection of fields declared in this library. + /// + /// This visitor removes a field when it finds that it is assigned anywhere. + final Map _fields; + + _FieldMutationFinder(this._fields); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _addMutatedFieldElement(node); + super.visitAssignmentExpression(node); + } + + @override + void visitPostfixExpression(PostfixExpression node) { + _addMutatedFieldElement(node); + super.visitPostfixExpression(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + var operator = node.operator; + if (operator.type == TokenType.MINUS_MINUS || + operator.type == TokenType.PLUS_PLUS) { + _addMutatedFieldElement(node); + } + super.visitPrefixExpression(node); + } + + void _addMutatedFieldElement(CompoundAssignmentExpression assignment) { + var element = assignment.writeElement?.canonicalElement; + if (element is FieldElement) { + _fields.remove(element); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCompilationUnit(CompilationUnit node) { + var declarationsCollector = _DeclarationsCollector(); + node.accept(declarationsCollector); + var fields = declarationsCollector.fields; + + var fieldMutationFinder = _FieldMutationFinder(fields); + for (var unit in context.allUnits) { + unit.unit.accept(fieldMutationFinder); + } + + for (var MapEntry(key: field, value: variable) in fields.entries) { + // TODO(srawlins): We could look at the constructors once and store a set + // of which fields are initialized by any, and a set of which fields are + // initialized by all. This would conceivably improve performance. + var classDeclaration = variable.parent?.parent?.parent; + var constructors = classDeclaration is ClassDeclaration + ? classDeclaration.members.whereType() + : []; + + var isSetInAnyConstructor = constructors + .any((constructor) => field.isSetInConstructor(constructor)); + + if (isSetInAnyConstructor) { + var isSetInEveryConstructor = constructors + .every((constructor) => field.isSetInConstructor(constructor)); + + if (isSetInEveryConstructor) { + rule.reportLint(variable, arguments: [variable.name.lexeme]); + } + } else if (field.hasInitializer) { + rule.reportLint(variable, arguments: [variable.name.lexeme]); + } + } + } +} + +extension on VariableElement { + bool get overridesField { + var enclosingElement = this.enclosingElement; + if (enclosingElement is! InterfaceElement) return false; + + var library = this.library; + if (library == null) return false; + + return enclosingElement.thisType + .lookUpSetter2(name, inherited: true, library) != + null; + } + + bool isSetInConstructor(ConstructorDeclaration constructor) => + constructor.initializers.any(isSetInInitializer) || + constructor.parameters.parameters.any(isSetInParameter); + + /// Whether `this` is initialized in [initializer]. + bool isSetInInitializer(ConstructorInitializer initializer) => + initializer is ConstructorFieldInitializer && + initializer.fieldName.canonicalElement == this; + + /// Whether `this` is initialized with [parameter]. + bool isSetInParameter(FormalParameter parameter) { + var formalField = parameter.declaredElement; + return formalField is FieldFormalParameterElement && + formalField.field == this; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_final_in_for_each.dart b/pkg/linter/lib/src/rules/prefer_final_in_for_each.dart new file mode 100644 index 00000000000..b62579a7814 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_final_in_for_each.dart @@ -0,0 +1,133 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Prefer final in for-each loop variable if reference is not reassigned.'; + +const _details = r''' +**DO** prefer declaring for-each loop variables as final if they are not +reassigned later in the code. + +Declaring for-each loop variables as final when possible is a good practice +because it helps avoid accidental reassignments and allows the compiler to do +optimizations. + +**BAD:** +```dart +for (var element in elements) { // LINT + print('Element: $element'); +} +``` + +**GOOD:** +```dart +for (final element in elements) { + print('Element: $element'); +} +``` + +**GOOD:** +```dart +for (var element in elements) { + element = element + element; + print('Element: $element'); +} +``` + +'''; + +class PreferFinalInForEach extends LintRule { + static const LintCode code = LintCode( + 'prefer_final_in_for_each', "The variable '{0}' should be final.", + correctionMessage: 'Try making the variable final.'); + + static const LintCode patternCode = LintCode( + 'prefer_final_in_for_each', 'The pattern should be final.', + correctionMessage: 'Try making the pattern final.'); + + PreferFinalInForEach() + : super( + name: 'prefer_final_in_for_each', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addForEachPartsWithDeclaration(this, visitor); + registry.addForEachPartsWithPattern(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { + var loopVariable = node.loopVariable; + if (loopVariable.isFinal) return; + + var function = node.thisOrAncestorOfType(); + var loopVariableElement = loopVariable.declaredElement; + if (function != null && + loopVariableElement != null && + !function.isPotentiallyMutatedInScope(loopVariableElement)) { + var name = loopVariable.name; + rule.reportLintForToken(name, arguments: [name.lexeme]); + } + } + + @override + void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { + if (node.keyword.isFinal) return; + + var function = node.thisOrAncestorOfType(); + if (function == null) return; + + var pattern = node.pattern; + if (pattern is RecordPattern) { + if (!function.potentiallyMutatesAnyField(pattern.fields)) { + rule.reportLint(pattern, errorCode: PreferFinalInForEach.patternCode); + } + } else if (pattern is ObjectPattern) { + if (!function.potentiallyMutatesAnyField(pattern.fields)) { + rule.reportLint(pattern, errorCode: PreferFinalInForEach.patternCode); + } + } else if (pattern is ListPattern) { + if (!pattern.elements.any((e) => function.potentiallyMutates(e))) { + rule.reportLint(pattern, errorCode: PreferFinalInForEach.patternCode); + } + } else if (pattern is MapPattern) { + if (!pattern.elements.any((e) => + e is! MapPatternEntry || function.potentiallyMutates(e.value))) { + rule.reportLint(pattern, errorCode: PreferFinalInForEach.patternCode); + } + } + } +} + +extension on FunctionBody { + bool potentiallyMutates(Object pattern) { + if (pattern is! DeclaredVariablePattern) return true; + var element = pattern.declaredElement; + if (element == null) return true; + return isPotentiallyMutatedInScope(element.declaration); + } + + bool potentiallyMutatesAnyField(List fields) => + fields.any((f) => potentiallyMutates(f.pattern)); +} diff --git a/pkg/linter/lib/src/rules/prefer_final_locals.dart b/pkg/linter/lib/src/rules/prefer_final_locals.dart new file mode 100644 index 00000000000..9305376830d --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_final_locals.dart @@ -0,0 +1,206 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Prefer final for variable declarations if they are not reassigned.'; + +const _details = r''' +**DO** prefer declaring variables as final if they are not reassigned later in +the code. + +Declaring variables as final when possible is a good practice because it helps +avoid accidental reassignments and allows the compiler to do optimizations. + +**BAD:** +```dart +void badMethod() { + var label = 'hola mundo! badMethod'; // LINT + print(label); +} +``` + +**GOOD:** +```dart +void goodMethod() { + final label = 'hola mundo! goodMethod'; + print(label); +} +``` + +**GOOD:** +```dart +void mutableCase() { + var label = 'hola mundo! mutableCase'; + print(label); + label = 'hello world'; + print(label); +} +``` + +'''; + +class PreferFinalLocals extends LintRule { + static const LintCode code = LintCode( + 'prefer_final_locals', 'Local variables should be final.', + correctionMessage: 'Try making the variable final.'); + + PreferFinalLocals() + : super( + name: 'prefer_final_locals', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['unnecessary_final']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDeclaredVariablePattern(this, visitor); + registry.addPatternVariableDeclaration(this, visitor); + registry.addVariableDeclarationList(this, visitor); + } +} + +class _DeclaredVariableVisitor extends RecursiveAstVisitor { + final List declaredElements = []; + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + var element = node.declaredElement; + if (element != null) { + declaredElements.add(element); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isPotentiallyMutated(AstNode pattern, FunctionBody function) { + if (pattern is DeclaredVariablePattern) { + var element = pattern.declaredElement; + if (element == null || function.isPotentiallyMutatedInScope(element)) { + return true; + } + } + return false; + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + // [DeclaredVariablePattern]s which are declared by a + // [PatternVariableDeclaration] are reported in + // [visitPatternVariableDeclaration]. + if (node.thisOrAncestorOfType() != null) return; + if (node.isDeclaredFinal) return; + + var function = node.thisOrAncestorOfType(); + if (function == null) return; + + var inCaseClause = node.thisOrAncestorOfType() != null; + if (inCaseClause) { + if (!isPotentiallyMutated(node, function)) { + rule.reportLint(node); + } + } else { + var forEachPattern = node.thisOrAncestorOfType(); + if (forEachPattern != null) { + if (forEachPattern + .hasPotentiallyMutatedDeclaredVariableInScope(function)) { + return; + } + } else { + if (isPotentiallyMutated(node, function)) return; + } + } + + if (!inCaseClause) { + rule.reportLint(node); + } + } + + @override + void visitPatternVariableDeclaration(PatternVariableDeclaration node) { + if (node.isDeclaredFinal) return; + if (node.keyword.isFinal) return; + + var function = node.thisOrAncestorOfType(); + if (function == null) return; + + var inCaseClause = node.thisOrAncestorOfType() != null; + + if (inCaseClause) { + if (!isPotentiallyMutated(node, function)) { + rule.reportLint(node); + } + } else { + if (!node.hasPotentiallyMutatedDeclaredVariableInScope(function)) { + rule.reportLintForToken(node.keyword); + } + } + } + + @override + void visitVariableDeclarationList(VariableDeclarationList node) { + if (node.isConst || node.isFinal) return; + + var function = node.thisOrAncestorOfType(); + if (function == null) return; + + for (var variable in node.variables) { + if (variable.equals == null || variable.initializer == null) { + return; + } + var declaredElement = variable.declaredElement; + if (declaredElement != null && + function.isPotentiallyMutatedInScope(declaredElement)) { + return; + } + } + if (node.keyword != null) { + rule.reportLintForToken(node.keyword); + } else if (node.type != null) { + rule.reportLint(node.type); + } + } +} + +extension on AstNode { + bool get isDeclaredFinal { + var self = this; + if (self is DeclaredVariablePattern) { + if (self.keyword.isFinal) return true; + var pattern = self.thisOrAncestorOfType(); + if (pattern != null && pattern.keyword.isFinal) return true; + } + return false; + } + + bool hasPotentiallyMutatedDeclaredVariableInScope(FunctionBody function) { + var declaredVariableVisitor = _DeclaredVariableVisitor(); + accept(declaredVariableVisitor); + var declaredElements = declaredVariableVisitor.declaredElements; + for (var element in declaredElements) { + if (function.isPotentiallyMutatedInScope(element)) { + return true; + } + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_final_parameters.dart b/pkg/linter/lib/src/rules/prefer_final_parameters.dart new file mode 100644 index 00000000000..16c7311b70c --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_final_parameters.dart @@ -0,0 +1,139 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Prefer final for parameter declarations if they are not reassigned.'; + +const _details = r''' +**DO** prefer declaring parameters as final if they are not reassigned in +the function body. + +Declaring parameters as final when possible is a good practice because it helps +avoid accidental reassignments. + +**BAD:** +```dart +void badParameter(String label) { // LINT + print(label); +} +``` + +**GOOD:** +```dart +void goodParameter(final String label) { // OK + print(label); +} +``` + +**BAD:** +```dart +void badExpression(int value) => print(value); // LINT +``` + +**GOOD:** +```dart +void goodExpression(final int value) => print(value); // OK +``` + +**BAD:** +```dart +[1, 4, 6, 8].forEach((value) => print(value + 2)); // LINT +``` + +**GOOD:** +```dart +[1, 4, 6, 8].forEach((final value) => print(value + 2)); // OK +``` + +**GOOD:** +```dart +void mutableParameter(String label) { // OK + print(label); + label = 'Hello Linter!'; + print(label); +} +``` + +'''; + +class PreferFinalParameters extends LintRule { + static const LintCode code = LintCode( + 'prefer_final_parameters', "The parameter '{0}' should be final.", + correctionMessage: 'Try making the parameter final.'); + + PreferFinalParameters() + : super( + name: 'prefer_final_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => + const ['unnecessary_final', 'avoid_final_parameters']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addFunctionExpression(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) => + _reportApplicableParameters(node.parameters, node.body); + + @override + void visitFunctionExpression(FunctionExpression node) => + _reportApplicableParameters(node.parameters, node.body); + + @override + void visitMethodDeclaration(MethodDeclaration node) => + _reportApplicableParameters(node.parameters, node.body); + + /// Report the lint for parameters in the [parameters] list that are not + /// const or final already and not potentially mutated in the function [body]. + void _reportApplicableParameters( + FormalParameterList? parameters, FunctionBody body) { + if (parameters != null) { + for (var param in parameters.parameters) { + if (param is DefaultFormalParameter) { + param = param.parameter; + } + if (param.isFinal || + param.isConst || + // A field formal parameter is final even without the `final` + // modifier. + param is FieldFormalParameter || + // A super formal parameter is final even without the `final` + // modifier. + param is SuperFormalParameter) { + continue; + } + var declaredElement = param.declaredElement; + if (declaredElement != null && + !declaredElement.isInitializingFormal && + !body.isPotentiallyMutatedInScope(declaredElement)) { + rule.reportLint(param, arguments: [param.name!.lexeme]); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_for_elements_to_map_fromIterable.dart b/pkg/linter/lib/src/rules/prefer_for_elements_to_map_fromIterable.dart new file mode 100644 index 00000000000..1faf7bfe8cd --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_for_elements_to_map_fromIterable.dart @@ -0,0 +1,150 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Prefer 'for' elements when building maps from iterables."; + +const _details = r''' +When building maps from iterables, it is preferable to use 'for' elements. + +Using 'for' elements brings several benefits including: + +- Performance +- Flexibility +- Readability +- Improved type inference +- Improved interaction with null safety + + +**BAD:** +```dart +Map.fromIterable( + kAllGalleryDemos, + key: (demo) => '${demo.routeName}', + value: (demo) => demo.buildRoute, +); + +``` + +**GOOD:** +```dart +return { + for (var demo in kAllGalleryDemos) + '${demo.routeName}': demo.buildRoute, +}; +``` + +**GOOD:** +```dart +// Map is not required, type is inferred automatically. +final pizzaRecipients = { + ...studentLeaders, + for (var student in classG) + if (student.isPassing) student.id: student, +}; +``` +'''; + +class PreferForElementsToMapFromIterable extends LintRule { + static const LintCode code = LintCode( + 'prefer_for_elements_to_map_fromIterable', + "Use 'for' elements when building maps from iterables.", + correctionMessage: + "Try using a collection literal with a 'for' element."); + + PreferForElementsToMapFromIterable() + : super( + name: 'prefer_for_elements_to_map_fromIterable', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression creation) { + var element = creation.constructorName.staticElement; + if (element == null || + element.name != 'fromIterable' || + element.enclosingElement != context.typeProvider.mapElement) { + return; + } + + // + // Ensure that the arguments have the right form. + // + var arguments = creation.argumentList.arguments; + if (arguments.length != 3) { + return; + } + + // TODO(srawlins): Handle named arguments anywhere. + var secondArg = arguments[1]; + var thirdArg = arguments[2]; + + var keyClosure = + _extractClosure('key', secondArg) ?? _extractClosure('key', thirdArg); + var valueClosure = _extractClosure('value', thirdArg) ?? + _extractClosure('value', secondArg); + if (keyClosure == null || valueClosure == null) { + return; + } + + rule.reportLint(creation); + } + + FunctionExpression? _extractClosure(String name, Expression argument) { + if (argument is NamedExpression && argument.name.label.name == name) { + var expression = argument.expression.unParenthesized; + if (expression is FunctionExpression) { + var parameters = expression.parameters?.parameters; + if (parameters != null && + parameters.length == 1 && + parameters.first.isRequired) { + if (expression.hasSingleExpressionBody) { + return expression; + } + } + } + } + return null; + } +} + +extension on FunctionExpression { + /// Whether this has a single expression body, which could be a single + /// return statement in a block function body. + bool get hasSingleExpressionBody { + var body = this.body; + if (body is ExpressionFunctionBody) { + return true; + } else if (body is BlockFunctionBody) { + var statements = body.block.statements; + if (statements.length == 1) { + return statements.first is ReturnStatement; + } + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_foreach.dart b/pkg/linter/lib/src/rules/prefer_foreach.dart new file mode 100644 index 00000000000..5b2e00f9868 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_foreach.dart @@ -0,0 +1,161 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use `forEach` to only apply a function to all the elements.'; + +const _details = r''' +**DO** use `forEach` if you are only going to apply a function or a method +to all the elements of an iterable. + +Using `forEach` when you are only going to apply a function or method to all +elements of an iterable is a good practice because it makes your code more +terse. + +**BAD:** +```dart +for (final key in map.keys.toList()) { + map.remove(key); +} +``` + +**GOOD:** +```dart +map.keys.toList().forEach(map.remove); +``` + +**NOTE:** Replacing a for each statement with a forEach call may change the +behavior in the case where there are side-effects on the iterable itself. +```dart +for (final v in myList) { + foo().f(v); // This code invokes foo() many times. +} + +myList.forEach(foo().f); // But this one invokes foo() just once. +``` + +'''; + +class PreferForeach extends LintRule { + static const LintCode code = LintCode('prefer_foreach', + "Use 'forEach' rather than a 'for' loop to apply a function to every element.", + correctionMessage: "Try using 'forEach' rather than a 'for' loop."); + + PreferForeach() + : super( + name: 'prefer_foreach', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addForStatement(this, visitor); + } +} + +class _PreferForEachVisitor extends SimpleAstVisitor { + final LintRule rule; + LocalVariableElement? element; + ForStatement? forEachStatement; + + _PreferForEachVisitor(this.rule); + + @override + void visitBlock(Block node) { + if (node.statements.length == 1) { + node.statements.first.accept(this); + } + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + node.expression.accept(this); + } + + @override + void visitForStatement(ForStatement node) { + var loopParts = node.forLoopParts; + if (loopParts is ForEachPartsWithDeclaration) { + var element = loopParts.loopVariable.declaredElement; + if (element != null) { + forEachStatement = node; + this.element = element; + node.body.accept(this); + } + } + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + var arguments = node.argumentList.arguments; + if (arguments.length == 1 && arguments.first.canonicalElement == element) { + rule.reportLint(forEachStatement); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var arguments = node.argumentList.arguments; + var target = node.target; + if (arguments.length == 1 && + arguments.first.canonicalElement == element && + (target == null || !_ReferenceFinder(element).references(target))) { + rule.reportLint(forEachStatement); + } + } + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + node.unParenthesized.accept(this); + } +} + +class _ReferenceFinder extends UnifyingAstVisitor { + bool found = false; + final LocalVariableElement? element; + _ReferenceFinder(this.element); + + bool references(Expression target) { + if (target.canonicalElement == element) return true; + + target.accept(this); + return found; + } + + @override + visitNode(AstNode node) { + if (found) return; + + found = node.canonicalElement == element; + if (!found) { + super.visitNode(node); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + _Visitor(this.rule); + + @override + void visitForStatement(ForStatement node) { + var loopParts = node.forLoopParts; + if (loopParts is ForEachParts) { + var visitor = _PreferForEachVisitor(rule); + node.accept(visitor); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_function_declarations_over_variables.dart b/pkg/linter/lib/src/rules/prefer_function_declarations_over_variables.dart new file mode 100644 index 00000000000..b8c01dd38ab --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_function_declarations_over_variables.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use a function declaration to bind a function to a name.'; + +const _details = r''' +**DO** use a function declaration to bind a function to a name. + +As Dart allows local function declarations, it is a good practice to use them in +the place of function literals. + +**BAD:** +```dart +void main() { + var localFunction = () { + ... + }; +} +``` + +**GOOD:** +```dart +void main() { + localFunction() { + ... + } +} +``` + +'''; + +class PreferFunctionDeclarationsOverVariables extends LintRule { + static const LintCode code = LintCode( + 'prefer_function_declarations_over_variables', + 'Use a function declaration rather than a variable assignment to bind a ' + 'function to a name.', + correctionMessage: + 'Try rewriting the closure assignment as a function declaration.'); + + PreferFunctionDeclarationsOverVariables() + : super( + name: 'prefer_function_declarations_over_variables', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addVariableDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (node.initializer is FunctionExpression) { + var function = node.thisOrAncestorOfType(); + var declaredElement = node.declaredElement; + if (function == null) { + // When there is no enclosing function body, this is a variable + // definition for a field or a top-level variable, which should only + // be reported if final. + if (node.isFinal) { + rule.reportLint(node); + } + } else { + if (declaredElement != null && + !function.isPotentiallyMutatedInScope(declaredElement)) { + rule.reportLint(node); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_generic_function_type_aliases.dart b/pkg/linter/lib/src/rules/prefer_generic_function_type_aliases.dart new file mode 100644 index 00000000000..af7770c803d --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_generic_function_type_aliases.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer generic function type aliases.'; + +const _details = r''' +**PREFER** generic function type aliases. + +With the introduction of generic functions, function type aliases +(`typedef void F()`) couldn't express all of the possible kinds of +parameterization that users might want to express. Generic function type aliases +(`typedef F = void Function()`) fixed that issue. + +For consistency and readability reasons, it's better to only use one syntax and +thus prefer generic function type aliases. + +**BAD:** +```dart +typedef void F(); +``` + +**GOOD:** +```dart +typedef F = void Function(); +``` + +'''; + +class PreferGenericFunctionTypeAliases extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode('prefer_generic_function_type_aliases', + "Use the generic function type syntax in 'typedef's.", + correctionMessage: "Try using the generic function type syntax ('{0}')."); + + PreferGenericFunctionTypeAliases() + : super( + name: 'prefer_generic_function_type_aliases', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionTypeAlias(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + //https://github.com/dart-lang/linter/issues/2777 + if (node.semicolon.isSynthetic) return; + + var returnType = node.returnType; + var typeParameters = node.typeParameters; + var parameters = node.parameters; + var returnTypeSource = + returnType == null ? '' : '${returnType.toSource()} '; + var typeParameterSource = + typeParameters == null ? '' : typeParameters.toSource(); + var parameterSource = parameters.toSource(); + var replacement = + '${returnTypeSource}Function$typeParameterSource$parameterSource'; + rule.reportLintForToken(node.name, arguments: [replacement]); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_if_elements_to_conditional_expressions.dart b/pkg/linter/lib/src/rules/prefer_if_elements_to_conditional_expressions.dart new file mode 100644 index 00000000000..24fc8a81f42 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_if_elements_to_conditional_expressions.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer if elements to conditional expressions where possible.'; + +const _details = r''' +When building collections, it is preferable to use `if` elements rather than +conditionals. + +**BAD:** +```dart +var list = ['a', 'b', condition ? 'c' : null].where((e) => e != null).toList(); +``` + +**GOOD:** +```dart +var list = ['a', 'b', if (condition) 'c']; +``` +'''; + +class PreferIfElementsToConditionalExpressions extends LintRule { + static const LintCode code = LintCode( + 'prefer_if_elements_to_conditional_expressions', + "Use an 'if' element to conditionally add elements.", + correctionMessage: + "Try using an 'if' element rather than a conditional expression."); + + PreferIfElementsToConditionalExpressions() + : super( + name: 'prefer_if_elements_to_conditional_expressions', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConditionalExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConditionalExpression(ConditionalExpression node) { + AstNode nodeToReplace = node; + var parent = node.parent; + while (parent is ParenthesizedExpression) { + nodeToReplace = parent; + parent = parent.parent; + } + if (parent is ListLiteral || (parent is SetOrMapLiteral && parent.isSet)) { + rule.reportLint(nodeToReplace); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_if_null_operators.dart b/pkg/linter/lib/src/rules/prefer_if_null_operators.dart new file mode 100644 index 00000000000..d6748be5880 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_if_null_operators.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer using if null operators.'; + +const _details = r''' +**PREFER** using if null operators instead of null checks in conditional +expressions. + +**BAD:** +```dart +v = a == null ? b : a; +``` + +**GOOD:** +```dart +v = a ?? b; +``` + +'''; + +class PreferIfNullOperators extends LintRule { + static const LintCode code = LintCode('prefer_if_null_operators', + "Use the '??' operator rather than '?:' when testing for 'null'.", + correctionMessage: "Try rewriting the code to use '??'."); + + PreferIfNullOperators() + : super( + name: 'prefer_if_null_operators', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConditionalExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConditionalExpression(ConditionalExpression node) { + var condition = node.condition; + if (condition is BinaryExpression && + (condition.operator.type == TokenType.EQ_EQ || + condition.operator.type == TokenType.BANG_EQ)) { + // ensure condition is a null check + Expression expression; + if (condition.leftOperand is NullLiteral) { + expression = condition.rightOperand; + } else if (condition.rightOperand is NullLiteral) { + expression = condition.leftOperand; + } else { + return; + } + + var exp = condition.operator.type == TokenType.EQ_EQ + ? node.elseExpression + : node.thenExpression; + if (exp.toString() == expression.toString()) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_initializing_formals.dart b/pkg/linter/lib/src/rules/prefer_initializing_formals.dart new file mode 100644 index 00000000000..ef287b6170f --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_initializing_formals.dart @@ -0,0 +1,239 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use initializing formals when possible.'; + +const _details = r''' +**DO** use initializing formals when possible. + +Using initializing formals when possible makes your code more terse. + +**BAD:** +```dart +class Point { + num x, y; + Point(num x, num y) { + this.x = x; + this.y = y; + } +} +``` + +**GOOD:** +```dart +class Point { + num x, y; + Point(this.x, this.y); +} +``` + +**BAD:** +```dart +class Point { + num x, y; + Point({num x, num y}) { + this.x = x; + this.y = y; + } +} +``` + +**GOOD:** +```dart +class Point { + num x, y; + Point({this.x, this.y}); +} +``` + +**NOTE:** +This rule will not generate a lint for named parameters unless the parameter +name and the field name are the same. The reason for this is that resolving +such a lint would require either renaming the field or renaming the parameter, +and both of those actions would potentially be a breaking change. For example, +the following will not generate a lint: + +```dart +class Point { + bool isEnabled; + Point({bool enabled}) { + this.isEnabled = enabled; // OK + } +} +``` + +**NOTE:** +Also note that it is possible to enforce a type that is stricter than the +initialized field with an initializing formal parameter. In the following +example the unnamed `Bid` constructor requires a non-null `int` despite +`amount` being declared nullable (`int?`). + +```dart +class Bid { + final int? amount; + Bid(int this.amount); + Bid.pass() : amount = null; +} +``` +'''; + +Iterable _getAssignmentExpressionsInConstructorBody( + ConstructorDeclaration node) { + var body = node.body; + if (body is! BlockFunctionBody) return []; + var assignments = []; + for (var statement in body.block.statements) { + if (statement is ExpressionStatement) { + var expression = statement.expression; + if (expression is AssignmentExpression) { + assignments.add(expression); + } + } + } + return assignments; +} + +Iterable + _getConstructorFieldInitializersInInitializers( + ConstructorDeclaration node) => + node.initializers.whereType(); + +Element? _getLeftElement(AssignmentExpression assignment) => + assignment.writeElement?.canonicalElement; + +Iterable _getParameters(ConstructorDeclaration node) => + node.parameters.parameters.map((e) => e.declaredElement); + +Element? _getRightElement(AssignmentExpression assignment) => + assignment.rightHandSide.canonicalElement; + +class PreferInitializingFormals extends LintRule { + static const LintCode code = LintCode('prefer_initializing_formals', + 'Use an initializing formal to assign a parameter to a field.', + correctionMessage: + "Try using an initialing formal ('this.{0}') to initialize the field."); + + PreferInitializingFormals() + : super( + name: 'prefer_initializing_formals', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + // Skip factory constructors. + // https://github.com/dart-lang/linter/issues/2441 + if (node.factoryKeyword != null) { + return; + } + + var parameters = _getParameters(node); + var parametersUsedOnce = {}; + var parametersUsedMoreThanOnce = {}; + + bool isAssignmentExpressionToLint(AssignmentExpression assignment) { + var leftElement = _getLeftElement(assignment); + var rightElement = _getRightElement(assignment); + return leftElement != null && + rightElement != null && + leftElement.name == rightElement.name && + !leftElement.isPrivate && + leftElement is FieldElement && + !leftElement.isSynthetic && + leftElement.enclosingElement == + node.declaredElement?.enclosingElement && + parameters.contains(rightElement) && + (!parametersUsedMoreThanOnce.contains(rightElement) && + !(rightElement as ParameterElement).isNamed || + leftElement.name == rightElement.name); + } + + bool isConstructorFieldInitializerToLint( + ConstructorFieldInitializer constructorFieldInitializer) { + var expression = constructorFieldInitializer.expression; + if (expression is SimpleIdentifier) { + var fieldName = constructorFieldInitializer.fieldName; + if (fieldName.name != expression.name) { + return false; + } + var staticElement = expression.staticElement; + return staticElement is ParameterElement && + !(constructorFieldInitializer.fieldName.staticElement?.isPrivate ?? + true) && + parameters.contains(staticElement) && + (!parametersUsedMoreThanOnce.contains(expression.staticElement) && + !staticElement.isNamed || + (constructorFieldInitializer.fieldName.staticElement?.name == + expression.staticElement?.name)); + } + return false; + } + + void processElement(Element? element) { + if (!parametersUsedOnce.add(element)) { + parametersUsedMoreThanOnce.add(element); + } + } + + var parameterElements = node.parameters.parameterElements; + for (var parameter in parameterElements) { + if (parameter?.isInitializingFormal ?? false) { + processElement(parameter); + } + } + + var assignments = _getAssignmentExpressionsInConstructorBody(node); + for (var assignment in assignments) { + if (isAssignmentExpressionToLint(assignment)) { + processElement(_getRightElement(assignment)); + } + } + + var initializers = _getConstructorFieldInitializersInInitializers(node); + for (var initializer in initializers) { + if (isConstructorFieldInitializerToLint(initializer)) { + processElement( + (initializer.expression as SimpleIdentifier).staticElement); + } + } + + for (var assignment in assignments) { + if (isAssignmentExpressionToLint(assignment)) { + var rightElement = _getRightElement(assignment)!; + rule.reportLint(assignment, arguments: [rightElement.displayName]); + } + } + + for (var initializer in initializers) { + if (isConstructorFieldInitializerToLint(initializer)) { + var name = initializer.fieldName.staticElement!.name!; + rule.reportLint(initializer, arguments: [name]); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_inlined_adds.dart b/pkg/linter/lib/src/rules/prefer_inlined_adds.dart new file mode 100644 index 00000000000..2713e9bb433 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_inlined_adds.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Inline list item declarations where possible.'; + +const _details = r''' +Declare elements in list literals inline, rather than using `add` and +`addAll` methods where possible. + + +**BAD:** +```dart +var l = ['a']..add('b')..add('c'); +var l2 = ['a']..addAll(['b', 'c']); +``` + +**GOOD:** +```dart +var l = ['a', 'b', 'c']; +var l2 = ['a', 'b', 'c']; +``` +'''; + +class PreferInlinedAdds extends LintRule { + static const LintCode single = LintCode( + 'prefer_inlined_adds', 'The addition of a list item could be inlined.', + correctionMessage: 'Try adding the item to the list literal directly.'); + + static const LintCode multiple = LintCode('prefer_inlined_adds', + 'The addition of multiple list items could be inlined.', + correctionMessage: 'Try adding the items to the list literal directly.'); + + PreferInlinedAdds() + : super( + name: 'prefer_inlined_adds', + description: _desc, + details: _details, + group: Group.style); + + @override + List get lintCodes => [multiple, single]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation invocation) { + var addAll = invocation.methodName.name == 'addAll'; + if ((invocation.methodName.name != 'add' && !addAll) || + !invocation.isCascaded || + invocation.argumentList.arguments.length != 1) { + return; + } + + var cascade = invocation.thisOrAncestorOfType(); + var sections = cascade?.cascadeSections; + var target = cascade?.target; + if (target is! ListLiteral || + (sections != null && sections.first != invocation)) { + // todo (pq): consider extending to handle set literals. + return; + } + + if (addAll && invocation.argumentList.arguments.first is! ListLiteral) { + // Handled by: prefer_spread_collections + return; + } + + rule.reportLint(invocation.methodName, + errorCode: + addAll ? PreferInlinedAdds.multiple : PreferInlinedAdds.single); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_int_literals.dart b/pkg/linter/lib/src/rules/prefer_int_literals.dart new file mode 100644 index 00000000000..edc6e0da33b --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_int_literals.dart @@ -0,0 +1,139 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = 'Prefer int literals over double literals.'; + +const _details = ''' +**DO** use int literals rather than the corresponding double literal. + +**BAD:** +```dart +const double myDouble = 8.0; +final anotherDouble = myDouble + 7.0e2; +main() { + someMethod(6.0); +} +``` + +**GOOD:** +```dart +const double myDouble = 8; +final anotherDouble = myDouble + 700; +main() { + someMethod(6); +} +``` + +'''; + +class PreferIntLiterals extends LintRule { + static const LintCode code = LintCode( + 'prefer_int_literals', "Unnecessary use of a 'double' literal.", + correctionMessage: "Try using an 'int' literal."); + + PreferIntLiterals() + : super( + name: 'prefer_int_literals', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + registry.addDoubleLiteral(this, _Visitor(this)); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + /// Determine if the given literal can be replaced by an int literal. + bool canReplaceWithIntLiteral(DoubleLiteral literal) { + var parent = literal.parent; + if (parent is PrefixExpression) { + if (parent.operator.lexeme == '-') { + return hasTypeDouble(parent); + } else { + return false; + } + } + return hasTypeDouble(literal); + } + + bool hasReturnTypeDouble(AstNode? node) { + if (node is FunctionExpression) { + var functionDeclaration = node.parent; + if (functionDeclaration is FunctionDeclaration) { + return _isDartCoreDoubleTypeAnnotation(functionDeclaration.returnType); + } + } else if (node is MethodDeclaration) { + return _isDartCoreDoubleTypeAnnotation(node.returnType); + } + return false; + } + + bool hasTypeDouble(Expression expression) { + var parent = expression.parent; + if (parent is ArgumentList) { + return _isDartCoreDouble(expression.staticParameterElement?.type); + } else if (parent is ListLiteral) { + var typeArguments = parent.typeArguments?.arguments; + return typeArguments?.length == 1 && + _isDartCoreDoubleTypeAnnotation(typeArguments!.first); + } else if (parent is NamedExpression) { + var argList = parent.parent; + if (argList is ArgumentList) { + return _isDartCoreDouble(parent.staticParameterElement?.type); + } + } else if (parent is ExpressionFunctionBody) { + return hasReturnTypeDouble(parent.parent); + } else if (parent is ReturnStatement) { + var body = parent.thisOrAncestorOfType(); + return body != null && hasReturnTypeDouble(body.parent); + } else if (parent is VariableDeclaration) { + var varList = parent.parent; + if (varList is VariableDeclarationList) { + return _isDartCoreDoubleTypeAnnotation(varList.type); + } + } + return false; + } + + @override + void visitDoubleLiteral(DoubleLiteral node) { + // Check if the double can be represented as an int + try { + var value = node.value; + if (value != value.truncate()) { + return; + } + // ignore: avoid_catching_errors + } on UnsupportedError catch (_) { + // The double cannot be represented as an int + return; + } + + // Ensure that replacing the double would not change the semantics + if (canReplaceWithIntLiteral(node)) { + rule.reportLint(node); + } + } + + bool _isDartCoreDouble(DartType? type) => type?.isDartCoreDouble ?? false; + + bool _isDartCoreDoubleTypeAnnotation(TypeAnnotation? annotation) => + _isDartCoreDouble(annotation?.type); +} diff --git a/pkg/linter/lib/src/rules/prefer_interpolation_to_compose_strings.dart b/pkg/linter/lib/src/rules/prefer_interpolation_to_compose_strings.dart new file mode 100644 index 00000000000..665987c06ae --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_interpolation_to_compose_strings.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Use interpolation to compose strings and values.'; + +const _details = r''' +**PREFER** using interpolation to compose strings and values. + +Using interpolation when composing strings and values is usually easier to write +and read than concatenation. + +**BAD:** +```dart +'Hello, ' + person.name + ' from ' + person.city + '.'; +``` + +**GOOD:** +```dart +'Hello, ${person.name} from ${person.city}.' +``` + +'''; + +class PreferInterpolationToComposeStrings extends LintRule { + static const LintCode code = LintCode( + 'prefer_interpolation_to_compose_strings', + 'Use interpolation to compose strings and values.', + correctionMessage: + 'Try using string interpolation to build the composite string.'); + + PreferInterpolationToComposeStrings() + : super( + name: 'prefer_interpolation_to_compose_strings', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBinaryExpression(this, visitor); + } +} + +class _NodeVisitor extends UnifyingAstVisitor { + Set skippedNodes; + _NodeVisitor(this.skippedNodes); + + @override + visitNode(AstNode node) { + skippedNodes.add(node); + + super.visitNode(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final skippedNodes = {}; + + _Visitor(this.rule); + + @override + void visitBinaryExpression(BinaryExpression node) { + if (skippedNodes.contains(node)) { + return; + } + if (node.operator.type == TokenType.PLUS) { + var leftOperand = node.leftOperand; + var rightOperand = node.rightOperand; + // OK(#735): `str1 + str2` + if (leftOperand is! StringLiteral && rightOperand is! StringLiteral) { + return; + } + // OK(#2490): `str1 + r''` + if (leftOperand is SimpleStringLiteral && leftOperand.isRaw || + rightOperand is SimpleStringLiteral && rightOperand.isRaw) { + return; + } + // OK: `'foo' + 'bar'` + if (leftOperand is StringLiteral && rightOperand is StringLiteral) { + return; + } + // OK(https://github.com/dart-lang/sdk/issues/52610): + // `a.toString(x: 0) + 'foo'` + // `'foo' + a.toString(x: 0)` + if (leftOperand.isToStringInvocationWithArguments || + rightOperand.isToStringInvocationWithArguments) { + return; + } + if (leftOperand.staticType?.isDartCoreString ?? false) { + rule.reportLint(node); + node.accept(_NodeVisitor(skippedNodes)); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_is_empty.dart b/pkg/linter/lib/src/rules/prefer_is_empty.dart new file mode 100644 index 00000000000..9d4cdb19ddf --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_is_empty.dart @@ -0,0 +1,242 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; + +const _desc = r'Use `isEmpty` for Iterables and Maps.'; +const _details = r''' +**DON'T** use `length` to see if a collection is empty. + +The `Iterable` contract does not require that a collection know its length or be +able to provide it in constant time. Calling `length` just to see if the +collection contains anything can be painfully slow. + +Instead, there are faster and more readable getters: `isEmpty` and +`isNotEmpty`. Use the one that doesn't require you to negate the result. + +**BAD:** +```dart +if (lunchBox.length == 0) return 'so hungry...'; +if (words.length != 0) return words.join(' '); +``` + +**GOOD:** +```dart +if (lunchBox.isEmpty) return 'so hungry...'; +if (words.isNotEmpty) return words.join(' '); +``` + +'''; + +class PreferIsEmpty extends LintRule { + // TODO(brianwilkerson) Both `alwaysFalse` and `alwaysTrue` should be warnings + // rather than lints because they represent a bug rather than a style + // preference. + static const LintCode alwaysFalse = LintCode( + 'prefer_is_empty', + "The comparison is always 'false' because the length is always greater " + 'than or equal to 0.'); + + static const LintCode alwaysTrue = LintCode( + 'prefer_is_empty', + "The comparison is always 'true' because the length is always greater " + 'than or equal to 0.'); + + static const LintCode useIsEmpty = LintCode( + 'prefer_is_empty', + "Use 'isEmpty' instead of 'length' to test whether the collection is " + 'empty.', + correctionMessage: "Try rewriting the expression to use 'isEmpty'."); + + static const LintCode useIsNotEmpty = LintCode( + 'prefer_is_empty', + "Use 'isNotEmpty' instead of 'length' to test whether the collection is " + 'empty.', + correctionMessage: "Try rewriting the expression to use 'isNotEmpty'."); + + PreferIsEmpty() + : super( + name: 'prefer_is_empty', + description: _desc, + details: _details, + group: Group.style); + + @override + List get lintCodes => + [alwaysFalse, alwaysTrue, useIsEmpty, useIsNotEmpty]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final PreferIsEmpty rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitBinaryExpression(BinaryExpression node) { + // todo(pq): not evaluating constants deliberately but we *should*. + // see: https://github.com/dart-lang/linter/issues/2818 + var value = getIntValue(node.rightOperand, null); + if (value != null) { + if (_isLengthAccess(node.leftOperand)) { + _check(node, value, constantOnRight: true); + } + } else { + value = getIntValue(node.leftOperand, null); + if (value != null) { + if (_isLengthAccess(node.rightOperand)) { + _check(node, value, constantOnRight: false); + } + } + } + } + + void _check(BinaryExpression expression, int value, + {required bool constantOnRight}) { + // Don't lint if we're in a const constructor initializer. + var constructorInitializer = + expression.thisOrAncestorOfType(); + if (constructorInitializer != null) { + var constructorDecl = constructorInitializer.parent; + if (constructorDecl is! ConstructorDeclaration || + constructorDecl.constKeyword != null) { + return; + } + } + + // Or in a const context. + // See: https://github.com/dart-lang/linter/issues/1719 + if (expression.inConstantContext) { + return; + } + + var operator = expression.operator; + if (value == 0) { + if (operator.type == TokenType.EQ_EQ || + operator.type == TokenType.LT_EQ) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty); + } else if (operator.type == TokenType.GT || + operator.type == TokenType.BANG_EQ) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty); + } else if (operator.type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse); + } else if (operator.type == TokenType.GT_EQ) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue); + } + } else if (value == 1) { + if (constantOnRight) { + // 'length >= 1' is same as 'isNotEmpty', + // and 'length < 1' is same as 'isEmpty' + if (operator.type == TokenType.GT_EQ) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty); + } else if (operator.type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty); + } + } else { + // '1 <= length' is same as 'isNotEmpty', + // and '1 > length' is same as 'isEmpty' + if (operator.type == TokenType.LT_EQ) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsNotEmpty); + } else if (operator.type == TokenType.GT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.useIsEmpty); + } + } + } else if (value < 0) { + if (constantOnRight) { + // 'length' is always >= 0, so comparing with negative makes no sense. + if (operator.type == TokenType.EQ_EQ || + operator.type == TokenType.LT_EQ || + operator.type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse); + } else if (operator.type == TokenType.BANG_EQ || + operator.type == TokenType.GT_EQ || + operator.type == TokenType.GT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue); + } + } else { + // 'length' is always >= 0, so comparing with negative makes no sense. + if (operator.type == TokenType.EQ_EQ || + operator.type == TokenType.GT_EQ || + operator.type == TokenType.GT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysFalse); + } else if (operator.type == TokenType.BANG_EQ || + operator.type == TokenType.LT_EQ || + operator.type == TokenType.LT) { + rule.reportLint(expression, errorCode: PreferIsEmpty.alwaysTrue); + } + } + } + } + + /// todo(pq): consider sharing + T? _drillDownTo(Expression expression, + {required bool ignoreParens, required bool ignoreAs}) { + var search = expression; + // ignore: literal_only_boolean_expressions + while (true) { + if (ignoreParens && search is ParenthesizedExpression) { + search = search.expression; + } else if (ignoreAs && search is AsExpression) { + search = search.expression; + } else { + break; + } + } + + return search is T ? search : null; + } + + bool _isLengthAccess(Expression operand) { + var node = _drillDownTo(operand, ignoreParens: true, ignoreAs: true); + if (node == null) { + return false; + } + + SimpleIdentifier? identifier; + InterfaceType? type; + + if (node is PrefixedIdentifier) { + identifier = node.identifier; + var operandType = node.prefix.staticType; + if (operandType is InterfaceType) { + type = operandType; + } + } else if (node is PropertyAccess) { + identifier = node.propertyName; + var parentType = node.target?.staticType; + if (parentType is InterfaceType) { + type = parentType; + } + } + + if (identifier?.name != 'length') { + return false; + } + + // Should be subtype of Iterable, Map or String. + if (type == null || + !type.implementsInterface('Iterable', 'dart.core') && + !type.implementsInterface('Map', 'dart.core') && + !type.isDartCoreString) { + return false; + } + + return true; + } +} diff --git a/pkg/linter/lib/src/rules/prefer_is_not_empty.dart b/pkg/linter/lib/src/rules/prefer_is_not_empty.dart new file mode 100644 index 00000000000..5b1407f25d4 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_is_not_empty.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart' + show PrefixExpression, PrefixedIdentifier, PropertyAccess, SimpleIdentifier; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Use `isNotEmpty` for Iterables and Maps.'; + +const _details = r''' +**PREFER** `x.isNotEmpty` to `!x.isEmpty` for `Iterable` and `Map` instances. + +When testing whether an iterable or map is empty, prefer `isNotEmpty` over +`!isEmpty` to improve code readability. + +**BAD:** +```dart +if (!sources.isEmpty) { + process(sources); +} +``` + +**GOOD:** +```dart +if (todo.isNotEmpty) { + sendResults(request, todo.isEmpty); +} +``` + +'''; + +class PreferIsNotEmpty extends LintRule { + static const LintCode code = LintCode('prefer_is_not_empty', + "Use 'isNotEmpty' rather than negating the result of 'isEmpty'.", + correctionMessage: "Try rewriting the expression to use 'isNotEmpty'."); + + PreferIsNotEmpty() + : super( + name: 'prefer_is_not_empty', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addPrefixExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitPrefixExpression(PrefixExpression node) { + // Should be prefixed w/ a "!". + var prefix = node.operator; + if (prefix.type != TokenType.BANG) { + return; + } + + var expression = node.operand.unParenthesized; + + // Should be a property access or prefixed identifier. + SimpleIdentifier? isEmptyIdentifier; + if (expression is PropertyAccess) { + isEmptyIdentifier = expression.propertyName; + } else if (expression is PrefixedIdentifier) { + isEmptyIdentifier = expression.identifier; + } + if (isEmptyIdentifier == null) { + return; + } + + // Element identifier should be "isEmpty". + var propertyElement = isEmptyIdentifier.staticElement; + if (propertyElement == null || 'isEmpty' != propertyElement.name) { + return; + } + + // Element should also support "isNotEmpty". + var propertyTarget = propertyElement.enclosingElement; + if (propertyTarget == null || + getChildren(propertyTarget, 'isNotEmpty').isEmpty) { + return; + } + + rule.reportLint(node); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_is_not_operator.dart b/pkg/linter/lib/src/rules/prefer_is_not_operator.dart new file mode 100644 index 00000000000..e36b8dba370 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_is_not_operator.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer is! operator.'; + +const _details = r''' +When checking if an object is not of a specified type, it is preferable to use the 'is!' operator. + +**BAD:** +```dart +if (!(foo is Foo)) { + ... +} +``` + +**GOOD:** +```dart +if (foo is! Foo) { + ... +} +``` + +'''; + +class PreferIsNotOperator extends LintRule { + static const LintCode code = LintCode( + 'prefer_is_not_operator', + "Use the 'is!' operator rather than negating the value of the 'is' " + 'operator.', + correctionMessage: + "Try rewriting the condition to use the 'is!' operator."); + + PreferIsNotOperator() + : super( + name: 'prefer_is_not_operator', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addIsExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitIsExpression(IsExpression node) { + // Return if it is `is!` expression + if (node.notOperator != null) { + return; + } + + var parent = node.parent; + // Check whether is expression is inside parenthesis + if (parent is ParenthesizedExpression) { + var prefixExpression = parent.parent; + // Check for NOT (!) operator + if (prefixExpression is PrefixExpression && + prefixExpression.operator.type == TokenType.BANG) { + rule.reportLint(prefixExpression); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_iterable_whereType.dart b/pkg/linter/lib/src/rules/prefer_iterable_whereType.dart new file mode 100644 index 00000000000..00787c8c681 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_iterable_whereType.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Prefer to use whereType on iterable.'; + +const _details = r''' +**PREFER** `iterable.whereType()` over `iterable.where((e) => e is T)`. + +**BAD:** +```dart +iterable.where((e) => e is MyClass); +``` + +**GOOD:** +```dart +iterable.whereType(); +``` + +'''; + +class PreferIterableWhereType extends LintRule { + static const LintCode code = LintCode('prefer_iterable_whereType', + "Use 'whereType' to select elements of a given type.", + correctionMessage: "Try rewriting the expression to use 'whereType'."); + + PreferIterableWhereType() + : super( + name: 'prefer_iterable_whereType', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.methodName.name != 'where') return; + var target = node.realTarget; + if (target == null || + !target.staticType.implementsInterface('Iterable', 'dart.core')) { + return; + } + + var args = node.argumentList.arguments; + if (args.length != 1) return; + + var arg = args.first; + if (arg is FunctionExpression) { + if (arg.parameters?.parameters.length != 1) return; + + var body = arg.body; + Expression? expression; + if (body is BlockFunctionBody) { + var statements = body.block.statements; + if (statements.length != 1) return; + var statement = body.block.statements.first; + if (statement is ReturnStatement) { + expression = statement.expression; + } + } else if (body is ExpressionFunctionBody) { + expression = body.expression; + } + expression = expression?.unParenthesized; + if (expression is IsExpression && expression.notOperator == null) { + var target = expression.expression; + if (target is SimpleIdentifier && + target.name == arg.parameters?.parameters.first.name?.lexeme) { + rule.reportLint(node.methodName); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_mixin.dart b/pkg/linter/lib/src/rules/prefer_mixin.dart new file mode 100644 index 00000000000..8701d8ded37 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_mixin.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _dartCollectionUri = 'dart.collection'; +const _dartConvertUri = 'dart.convert'; + +const _desc = r'Prefer using mixins.'; + +const _details = r''' +Dart 2.1 introduced a new syntax for mixins that provides a safe way for a mixin +to invoke inherited members using `super`. The new style of mixins should always +be used for types that are to be mixed in. As a result, this lint will flag any +uses of a class in a `with` clause. + +**BAD:** +```dart +class A {} +class B extends Object with A {} +``` + +**OK:** +```dart +mixin M {} +class C with M {} +``` + +'''; + +const _iterableMixinName = 'IterableMixin'; +const _listMixinName = 'ListMixin'; +const _mapMixinName = 'MapMixin'; +const _setMixinName = 'SetMixin'; +const _stringConversionSinkName = 'StringConversionSinkMixin'; + +class PreferMixin extends LintRule { + static const LintCode code = LintCode( + 'prefer_mixin', 'Only mixins should be mixed in.', + correctionMessage: "Try converting '{0}' to a mixin."); + + PreferMixin() + : super( + name: 'prefer_mixin', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addWithClause(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitWithClause(WithClause node) { + for (var mixinNode in node.mixinTypes) { + var type = mixinNode.type; + if (type is InterfaceType) { + var element = type.element; + if (element is MixinElement) continue; + if ((element is ClassElement && !element.isMixinClass) && + !isAllowed(element)) { + rule.reportLint(mixinNode, arguments: [mixinNode.name2.lexeme]); + } + } + } + } + + /// Check for "legacy" classes that cannot easily be made `mixin`s for + /// compatibility reasons. + /// (See: https://github.com/dart-lang/linter/issues/2082) + static bool isAllowed(InterfaceElement element) => + // todo (pq): remove allowlist once legacy mixins are otherwise annotated. + // see: https://github.com/dart-lang/sdk/issues/45343 + + element.isClass(_iterableMixinName, _dartCollectionUri) || + element.isClass(_listMixinName, _dartCollectionUri) || + element.isClass(_mapMixinName, _dartCollectionUri) || + element.isClass(_setMixinName, _dartCollectionUri) || + element.isClass(_stringConversionSinkName, _dartConvertUri); +} diff --git a/pkg/linter/lib/src/rules/prefer_null_aware_method_calls.dart b/pkg/linter/lib/src/rules/prefer_null_aware_method_calls.dart new file mode 100644 index 00000000000..deefbaaff1f --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_null_aware_method_calls.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer null aware method calls.'; + +const _details = r''' +Instead of checking nullability of a function/method `f` before calling it you +can use `f?.call()`. + +**BAD:** +```dart +if (f != null) f!(); +``` + +**GOOD:** +```dart +f?.call(); +``` + +'''; + +class PreferNullAwareMethodCalls extends LintRule { + static const LintCode code = LintCode('prefer_null_aware_method_calls', + "Use a null-aware invocation of the 'call' method rather than explicitly testing for 'null'.", + correctionMessage: "Try using '?.call()' to invoke the function."); + + PreferNullAwareMethodCalls() + : super( + name: 'prefer_null_aware_method_calls', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addIfStatement(this, visitor); + registry.addConditionalExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConditionalExpression(ConditionalExpression node) { + var condition = node.condition; + if (condition is BinaryExpression && + condition.operator.type == TokenType.BANG_EQ && + condition.rightOperand is NullLiteral && + node.elseExpression is NullLiteral) { + _check(node.thenExpression, condition); + } + } + + @override + void visitIfStatement(IfStatement node) { + var condition = node.expression; + if (condition is BinaryExpression && + condition.operator.type == TokenType.BANG_EQ && + condition.rightOperand is NullLiteral && + node.elseKeyword == null) { + var then = node.thenStatement; + if (then is Block) { + if (then.statements.length != 1) { + return; + } + then = then.statements.first; + } + if (then is ExpressionStatement) { + _check(then.expression, condition); + } + } + } + + void _check(Expression expression, BinaryExpression condition) { + if (expression is FunctionExpressionInvocation) { + var target = expression.function; + if (target is PostfixExpression && + target.operator.type == TokenType.BANG && + target.operand.toSource() == condition.leftOperand.toSource()) { + rule.reportLint(expression); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_null_aware_operators.dart b/pkg/linter/lib/src/rules/prefer_null_aware_operators.dart new file mode 100644 index 00000000000..5628f8ca61f --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_null_aware_operators.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer using null aware operators.'; + +const _details = r''' +**PREFER** using null aware operators instead of null checks in conditional +expressions. + +**BAD:** +```dart +v = a == null ? null : a.b; +``` + +**GOOD:** +```dart +v = a?.b; +``` + +'''; + +class PreferNullAwareOperators extends LintRule { + static const LintCode code = LintCode( + 'prefer_null_aware_operators', + "Use the null-aware operator '?.' rather than an explicit 'null' " + 'comparison.', + correctionMessage: "Try using '?.'."); + + PreferNullAwareOperators() + : super( + name: 'prefer_null_aware_operators', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConditionalExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConditionalExpression(ConditionalExpression node) { + var condition = node.condition; + if (condition is! BinaryExpression) { + return; + } + + // Ensure the condition is a null check. + String conditionText; + if (condition.leftOperand is NullLiteral) { + conditionText = condition.rightOperand.toString(); + } else if (condition.rightOperand is NullLiteral) { + conditionText = condition.leftOperand.toString(); + } else { + return; + } + + Expression? resultExpression; + if (condition.operator.type == TokenType.EQ_EQ) { + // Ensure the expression is `x == null ? null : y`. + if (node.thenExpression is! NullLiteral) return; + + resultExpression = node.elseExpression; + } else if (condition.operator.type == TokenType.BANG_EQ) { + // Ensure the expression is `x != null ? y : null`. + if (node.elseExpression is! NullLiteral) return; + + resultExpression = node.thenExpression; + } else { + return; + } + + while (resultExpression != null) { + resultExpression = switch (resultExpression) { + PrefixedIdentifier() => resultExpression.prefix, + MethodInvocation() => resultExpression.target, + PostfixExpression() + when resultExpression.operator.type == TokenType.BANG => + resultExpression.operand, + PropertyAccess() => resultExpression.target, + _ => null, + }; + if (resultExpression.toString() == conditionText) { + rule.reportLint(node); + return; + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_relative_imports.dart b/pkg/linter/lib/src/rules/prefer_relative_imports.dart new file mode 100644 index 00000000000..b5c101a6417 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_relative_imports.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:path/path.dart' as path; + +import '../analyzer.dart'; +import '../ast.dart'; +import 'implementation_imports.dart' show samePackage; + +const _desc = r'Prefer relative imports for files in `lib/`.'; + +const _details = r''' +**PREFER** relative imports for files in `lib/`. + +When mixing relative and absolute imports it's possible to create confusion +where the same member gets imported in two different ways. One way to avoid +that is to ensure you consistently use relative imports for files within the +`lib/` directory. + +**BAD:** +```dart +import 'package:my_package/bar.dart'; +``` + +**GOOD:** +```dart +import 'bar.dart'; +``` + +'''; + +class PreferRelativeImports extends LintRule { + static const LintCode code = LintCode('prefer_relative_imports', + "Use relative imports for files in the 'lib' directory.", + correctionMessage: 'Try converting the URI to a relative URI.'); + + PreferRelativeImports() + : super( + name: 'prefer_relative_imports', + description: _desc, + details: _details, + group: Group.errors); + + @override + List get incompatibleRules => const ['always_use_package_imports']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!isInLibDir(context.currentUnit.unit, context.package)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addImportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final PreferRelativeImports rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + bool isPackageSelfReference(ImportDirective node) { + var uri = node.element?.uri; + if (uri is! DirectiveUriWithSource) { + return false; + } + + // Is it a package: import? + var importUri = uri.relativeUri; + if (!importUri.isScheme('package')) return false; + + var sourceUri = node.element?.source.uri; + if (!samePackage(importUri, sourceUri)) return false; + + // todo (pq): context.package.contains(source) should work (but does not) + var packageRoot = context.package?.root; + return packageRoot != null && + path.isWithin(packageRoot, uri.source.fullName); + } + + @override + void visitImportDirective(ImportDirective node) { + if (isPackageSelfReference(node)) { + rule.reportLint(node.uri); + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_single_quotes.dart b/pkg/linter/lib/src/rules/prefer_single_quotes.dart new file mode 100644 index 00000000000..06d74c99f00 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_single_quotes.dart @@ -0,0 +1,166 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Only use double quotes for strings containing single quotes.'; + +const _details = ''' +**DO** use single quotes where they wouldn't require additional escapes. + +That means strings with an apostrophe may use double quotes so that the +apostrophe isn't escaped (note: we don't lint the other way around, ie, a single +quoted string with an escaped apostrophe is not flagged). + +It's also rare, but possible, to have strings within string interpolations. In +this case, its much more readable to use a double quote somewhere. So double +quotes are allowed either within, or containing, an interpolated string literal. +Arguably strings within string interpolations should be its own type of lint. + +**BAD:** +```dart +useStrings( + "should be single quote", + r"should be single quote", + r"""should be single quotes""") +``` + +**GOOD:** +```dart +useStrings( + 'should be single quote', + r'should be single quote', + r\'''should be single quotes\''', + "here's ok", + "nested \${a ? 'strings' : 'can'} be wrapped by a double quote", + 'and nested \${a ? "strings" : "can be double quoted themselves"}'); +``` + +'''; + +class PreferSingleQuotes extends LintRule { + static const LintCode code = LintCode( + 'prefer_single_quotes', 'Unnecessary use of double quotes.', + correctionMessage: + 'Try using single quotes unless the string contains single quotes.'); + + PreferSingleQuotes() + : super( + name: 'prefer_single_quotes', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => const ['prefer_double_quotes']; + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = QuoteVisitor(this, useSingle: true); + registry.addSimpleStringLiteral(this, visitor); + registry.addStringInterpolation(this, visitor); + } +} + +class QuoteVisitor extends SimpleAstVisitor { + final LintRule rule; + final bool useSingle; + + QuoteVisitor( + this.rule, { + required this.useSingle, + }); + + /// Strings interpolations can contain other string nodes. Check like this. + bool containsString(StringInterpolation string) { + var checkHasString = _IsOrContainsStringVisitor(); + return string.elements + .any((child) => child.accept(checkHasString) ?? false); + } + + /// Strings can be within interpolations (ie, nested). Check like this. + bool isNestedString(AstNode node) => + // careful: node.getAncestor will check the node itself. + node.parent?.thisOrAncestorOfType() != null; + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (useSingle && (node.isSingleQuoted || node.value.contains("'")) || + !useSingle && (!node.isSingleQuoted || node.value.contains('"'))) { + return; + } + + // Bail out on 'strings ${x ? "containing" : "other"} strings' + if (!isNestedString(node)) { + rule.reportLintForToken(node.literal); + } + } + + @override + void visitStringInterpolation(StringInterpolation node) { + if (useSingle && node.isSingleQuoted || + !useSingle && !node.isSingleQuoted) { + return; + } + + // slightly more complicated check there are no single quotes + if (node.elements.any((e) => + e is InterpolationString && + (useSingle && e.value.contains("'") || + !useSingle && e.value.contains('"')))) { + return; + } + + // Bail out on "strings ${x ? 'containing' : 'other'} strings" + if (!containsString(node) && !isNestedString(node)) { + rule.reportLint(node); + } + } +} + +/// The only way to get immediate children in a unified, typesafe way, is to +/// call visitChildren on that node, and pass in a visitor. This collects at the +/// top level and stops. +class _ImmediateChildrenVisitor extends UnifyingAstVisitor { + final _children = []; + + @override + void visitNode(AstNode node) { + _children.add(node); + } + + static List childrenOf(AstNode node) { + var visitor = _ImmediateChildrenVisitor(); + node.visitChildren(visitor); + return visitor._children; + } +} + +/// Do a top-down analysis to search for string nodes. Note, do not pass in +/// string nodes directly to this visitor, or you will always get true. Pass in +/// its children. +class _IsOrContainsStringVisitor extends UnifyingAstVisitor { + /// Different way to express `accept` in a way that's clearer in this visitor. + bool isOrContainsString(AstNode node) => node.accept(this) ?? false; + + /// Scan as little of the tree as possible, by bailing out on first match. For + /// all leaf nodes, they will either have a method defined here and return + /// true, or they will return false because leaves have no children. + @override + bool visitNode(AstNode node) => + _ImmediateChildrenVisitor.childrenOf(node).any(isOrContainsString); + + @override + bool visitSimpleStringLiteral(SimpleStringLiteral string) => true; + + @override + bool visitStringInterpolation(StringInterpolation string) => true; +} diff --git a/pkg/linter/lib/src/rules/prefer_spread_collections.dart b/pkg/linter/lib/src/rules/prefer_spread_collections.dart new file mode 100644 index 00000000000..cb3a9e84a53 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_spread_collections.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/src/dart/ast/ast.dart'; // ignore: implementation_imports + +import '../analyzer.dart'; + +const _desc = r'Use spread collections when possible.'; + +const _details = r''' +Use spread collections when possible. + +Collection literals are excellent when you want to create a new collection out +of individual items. But, when existing items are already stored in another +collection, spread collection syntax leads to simpler code. + +**BAD:** + +```dart +Widget build(BuildContext context) { + return CupertinoPageScaffold( + child: ListView( + children: [ + Tab2Header(), + ]..addAll(buildTab2Conversation()), + ), + ); +} +``` + +```dart +var ints = [1, 2, 3]; +print(['a']..addAll(ints.map((i) => i.toString()))..addAll(['c'])); +``` + +```dart +var things; +var l = ['a']..addAll(things ?? const []); +``` + + +**GOOD:** + +```dart +Widget build(BuildContext context) { + return CupertinoPageScaffold( + child: ListView( + children: [ + Tab2Header(), + ...buildTab2Conversation(), + ], + ), + ); +} +``` + +```dart +var ints = [1, 2, 3]; +print(['a', ...ints.map((i) => i.toString()), 'c'); +``` + +```dart +var things; +var l = ['a', ...?things]; +``` +'''; + +class PreferSpreadCollections extends LintRule { + static const LintCode code = LintCode('prefer_spread_collections', + 'The addition of multiple elements could be inlined.', + correctionMessage: + "Try using the spread operator ('...') to inline the addition."); + + PreferSpreadCollections() + : super( + name: 'prefer_spread_collections', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation invocation) { + if (invocation.methodName.name != 'addAll' || + !invocation.isCascaded || + invocation.argumentList.arguments.length != 1) { + return; + } + + var cascade = invocation.thisOrAncestorOfType(); + var sections = cascade?.cascadeSections; + var target = cascade?.target; + // todo (pq): add support for Set literals. + if (target is! ListLiteral || + (target is ListLiteralImpl && target.inConstantContext) || + (sections != null && sections.first != invocation)) { + return; + } + + var argument = invocation.argumentList.arguments.first; + if (argument is ListLiteral) { + // Handled by: prefer_inlined_adds + return; + } + + rule.reportLint(invocation.methodName); + } +} diff --git a/pkg/linter/lib/src/rules/prefer_typing_uninitialized_variables.dart b/pkg/linter/lib/src/rules/prefer_typing_uninitialized_variables.dart new file mode 100644 index 00000000000..ed9d0344b9e --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_typing_uninitialized_variables.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer typing uninitialized variables and fields.'; + +const _details = r''' +**PREFER** specifying a type annotation for uninitialized variables and fields. + +Forgoing type annotations for uninitialized variables is a bad practice because +you may accidentally assign them to a type that you didn't originally intend to. + +**BAD:** +```dart +class BadClass { + static var bar; // LINT + var foo; // LINT + + void method() { + var bar; // LINT + bar = 5; + print(bar); + } +} +``` + +**BAD:** +```dart +void aFunction() { + var bar; // LINT + bar = 5; + ... +} +``` + +**GOOD:** +```dart +class GoodClass { + static var bar = 7; + var foo = 42; + int baz; // OK + + void method() { + int baz; + var bar = 5; + ... + } +} +``` + +'''; + +class PreferTypingUninitializedVariables extends LintRule { + static const LintCode forField = LintCode( + 'prefer_typing_uninitialized_variables', + 'An uninitialized field should have an explicit type annotation.', + correctionMessage: 'Try adding a type annotation.'); + + static const LintCode forVariable = LintCode( + 'prefer_typing_uninitialized_variables', + 'An uninitialized variable should have an explicit type annotation.', + correctionMessage: 'Try adding a type annotation.'); + + PreferTypingUninitializedVariables() + : super( + name: 'prefer_typing_uninitialized_variables', + description: _desc, + details: _details, + group: Group.style); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addVariableDeclarationList(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitVariableDeclarationList(VariableDeclarationList node) { + if (node.type != null) { + return; + } + + var code = node.parent is FieldDeclaration + ? PreferTypingUninitializedVariables.forField + : PreferTypingUninitializedVariables.forVariable; + + for (var v in node.variables) { + if (v.initializer == null) { + rule.reportLint(v, errorCode: code); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/prefer_void_to_null.dart b/pkg/linter/lib/src/rules/prefer_void_to_null.dart new file mode 100644 index 00000000000..0bdd407d068 --- /dev/null +++ b/pkg/linter/lib/src/rules/prefer_void_to_null.dart @@ -0,0 +1,153 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r"Don't use the Null type, unless you are positive that you don't want void."; + +const _details = r''' +**DON'T** use the type Null where void would work. + +**BAD:** +```dart +Null f() {} +Future f() {} +Stream f() {} +f(Null x) {} +``` + +**GOOD:** +```dart +void f() {} +Future f() {} +Stream f() {} +f(void x) {} +``` + +Some exceptions include formulating special function types: + +```dart +Null Function(Null, Null); +``` + +and for making empty literals which are safe to pass into read-only locations +for any type of map or list: + +```dart +[]; +{}; +``` +'''; + +class PreferVoidToNull extends LintRule { + static const LintCode code = LintCode( + 'prefer_void_to_null', "Unnecessary use of the type 'Null'.", + correctionMessage: "Try using 'void' instead."); + + PreferVoidToNull() + : super( + name: 'prefer_void_to_null', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addNamedType(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + _Visitor(this.rule, this.context); + + bool isFutureOrVoid(DartType type) { + if (!type.isDartAsyncFutureOr) return false; + if (type is! InterfaceType) return false; + return type.typeArguments.first is VoidType; + } + + bool isVoidIncompatibleOverride(MethodDeclaration parent, AstNode node) { + // Make sure we're checking a return type. + if (parent.returnType?.offset != node.offset) return false; + + var member = + context.inheritanceManager.overriddenMember(parent.declaredElement); + if (member == null) return false; + + var returnType = member.returnType; + if (returnType is VoidType) return false; + if (isFutureOrVoid(returnType)) return false; + + return true; + } + + @override + void visitNamedType(NamedType node) { + var nodeType = node.type; + if (nodeType == null || !nodeType.isDartCoreNull) { + return; + } + + var parent = node.parent; + + // Null Function() + if (parent is GenericFunctionType) return; + + // case var _ as Null + if (parent is CastPattern) return; + + // a as Null + if (parent is AsExpression) return; + + // Function(Null) + if (parent is SimpleFormalParameter && + parent.parent is FormalParameterList && + parent.parent?.parent is GenericFunctionType) { + return; + } + + if (parent is VariableDeclarationList && + node == parent.type && + parent.parent is! FieldDeclaration) { + // We should not recommend to use `void` for local or top-level variables. + return; + } + + // [] or {} + if (parent is TypeArgumentList) { + var literal = parent.parent; + if (literal is ListLiteral && literal.elements.isEmpty) { + return; + } else if (literal is SetOrMapLiteral && literal.elements.isEmpty) { + return; + } + } + + // extension _ on Null {} + if (parent is ExtensionDeclaration) { + return; + } + + // https://github.com/dart-lang/linter/issues/2792 + if (parent is MethodDeclaration && + isVoidIncompatibleOverride(parent, node)) { + return; + } + + rule.reportLintForToken(node.name2); + } +} diff --git a/pkg/linter/lib/src/rules/provide_deprecation_message.dart b/pkg/linter/lib/src/rules/provide_deprecation_message.dart new file mode 100644 index 00000000000..2b0c54ff732 --- /dev/null +++ b/pkg/linter/lib/src/rules/provide_deprecation_message.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Provide a deprecation message, via @Deprecated("message").'; + +const _details = r''' +**DO** specify a deprecation message (with migration instructions and/or a +removal schedule) in the Deprecation constructor. + +**BAD:** +```dart +@deprecated +void oldFunction(arg1, arg2) {} +``` + +**GOOD:** +```dart +@Deprecated(""" +[oldFunction] is being deprecated in favor of [newFunction] (with slightly +different parameters; see [newFunction] for more information). [oldFunction] +will be removed on or after the 4.0.0 release. +""") +void oldFunction(arg1, arg2) {} +``` + +'''; + +class ProvideDeprecationMessage extends LintRule { + static const LintCode code = LintCode( + 'provide_deprecation_message', 'Missing a deprecation message.', + correctionMessage: + "Try using the constructor to provide a message ('@Deprecated(\"message\")')."); + + ProvideDeprecationMessage() + : super( + name: 'provide_deprecation_message', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAnnotation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAnnotation(Annotation node) { + var elementAnnotation = node.elementAnnotation; + if (elementAnnotation != null && + elementAnnotation.isDeprecated && + node.arguments == null) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart b/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart new file mode 100644 index 00000000000..ab344dcb205 --- /dev/null +++ b/pkg/linter/lib/src/rules/pub/depend_on_referenced_packages.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../../analyzer.dart'; +import '../../ast.dart'; + +const _desc = r'Depend on referenced packages.'; + +const _details = r''' +**DO** depend on referenced packages. + +When importing a package, add a dependency on it to your pubspec. + +Depending explicitly on packages that you reference ensures they will always +exist and allows you to put a dependency constraint on them to guard you +against breaking changes. + +Whether this should be a regular dependency or dev_dependency depends on if it +is referenced from a public file (one under either `lib` or `bin`), or some +other private file. + +**BAD:** +```dart +import 'package:a/a.dart'; +``` + +```yaml +dependencies: +``` + +**GOOD:** +```dart +import 'package:a/a.dart'; +``` + +```yaml +dependencies: + a: ^1.0.0 +``` + +'''; + +class DependOnReferencedPackages extends LintRule { + static const LintCode code = LintCode('depend_on_referenced_packages', + "The imported package '{0}' isn't a dependency of the importing package.", + correctionMessage: + "Try adding a dependency for '{0}' in the 'pubspec.yaml' file."); + + DependOnReferencedPackages() + : super( + name: 'depend_on_referenced_packages', + description: _desc, + details: _details, + group: Group.pub); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + // Only lint if we have a pubspec. + var package = context.package; + if (package is! PubWorkspacePackage) return; + var pubspec = package.pubspec; + if (pubspec == null) return; + var name = pubspec.name?.value.text; + if (name == null) return; + + var dependencies = pubspec.dependencies; + var devDependencies = pubspec.devDependencies; + var availableDeps = [ + name, + if (dependencies != null) + for (var dep in dependencies) + if (dep.name?.text != null) dep.name!.text!, + if (devDependencies != null && + !isInPublicDir(context.currentUnit.unit, context.package)) + for (var dep in devDependencies) + if (dep.name?.text != null) dep.name!.text!, + ]; + + var visitor = _Visitor(this, availableDeps); + registry.addImportDirective(this, visitor); + registry.addExportDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + /// Virtual packages will not have explicit dependencies + /// and get skipped. + static const virtualPackages = [ + //https://github.com/dart-lang/linter/issues/3308 + 'flutter_gen', + ]; + + final DependOnReferencedPackages rule; + final List availableDeps; + + _Visitor(this.rule, this.availableDeps); + + @override + void visitExportDirective(ExportDirective node) => _checkDirective(node); + + @override + void visitImportDirective(ImportDirective node) => _checkDirective(node); + + void _checkDirective(UriBasedDirective node) { + // Is it a package: uri? + var uriContent = node.uri.stringValue; + if (uriContent == null) return; + if (!uriContent.startsWith('package:')) return; + + // The package name is the first segment of the uri, find the first slash. + var firstSlash = uriContent.indexOf('/'); + if (firstSlash == -1) return; + + var packageName = uriContent.substring(8, firstSlash); + if (virtualPackages.contains(packageName)) return; + if (availableDeps.contains(packageName)) return; + rule.reportLint(node.uri, arguments: [packageName]); + } +} diff --git a/pkg/linter/lib/src/rules/pub/package_names.dart b/pkg/linter/lib/src/rules/pub/package_names.dart new file mode 100644 index 00000000000..bb674c247be --- /dev/null +++ b/pkg/linter/lib/src/rules/pub/package_names.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../../analyzer.dart'; +import '../../utils.dart'; + +const _desc = r'Use `lowercase_with_underscores` for package names.'; + +const _details = r''' +From the [Pubspec format description](https://dart.dev/tools/pub/pubspec): + +**DO** use `lowercase_with_underscores` for package names. + +Package names should be all lowercase, with underscores to separate words, +`just_like_this`. Use only basic Latin letters and Arabic digits: [a-z0-9_]. +Also, make sure the name is a valid Dart identifier -- that it doesn't start +with digits and isn't a reserved word. + +'''; + +class PackageNames extends LintRule { + static const LintCode code = LintCode( + 'package_names', "The package name '{0}' isn't a snake_case identifier.", + correctionMessage: + 'Try changing the name to follow the snake_case style.'); + + PackageNames() + : super( + name: 'package_names', + description: _desc, + details: _details, + group: Group.pub); + + @override + LintCode get lintCode => code; + + @override + PubspecVisitor getPubspecVisitor() => Visitor(this); +} + +class Visitor extends PubspecVisitor { + final LintRule rule; + + Visitor(this.rule); + + @override + void visitPackageName(PSEntry name) { + var packageName = name.value.text; + if (packageName != null && !isValidPackageName(packageName)) { + rule.reportPubLint(name.value, arguments: [packageName]); + } + } +} diff --git a/pkg/linter/lib/src/rules/pub/secure_pubspec_urls.dart b/pkg/linter/lib/src/rules/pub/secure_pubspec_urls.dart new file mode 100644 index 00000000000..d887ecd469f --- /dev/null +++ b/pkg/linter/lib/src/rules/pub/secure_pubspec_urls.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/pub.dart'; // ignore: implementation_imports + +import '../../analyzer.dart'; + +const _desc = r'Use secure urls in `pubspec.yaml`.'; + +const _details = r''' +**DO** Use secure urls in `pubspec.yaml`. + +Use `https` instead of `http` or `git:`. + +**BAD:** +```yaml +repository: http://github.com/dart-lang/example +``` + +```yaml +git: + url: git://github.com/dart-lang/example/example.git +``` + +**GOOD:** +```yaml +repository: https://github.com/dart-lang/example +``` + +'''; + +class SecurePubspecUrls extends LintRule { + static const LintCode code = LintCode('secure_pubspec_urls', + "The '{0}' protocol shouldn't be used because it isn't secure.", + correctionMessage: "Try using a secure protocol, such as 'https'."); + + SecurePubspecUrls() + : super( + name: 'secure_pubspec_urls', + description: _desc, + details: _details, + group: Group.pub); + + @override + LintCode get lintCode => code; + + @override + PubspecVisitor getPubspecVisitor() => Visitor(this); +} + +class Visitor extends PubspecVisitor { + final LintRule rule; + + Visitor(this.rule); + + @override + void visitPackageDependencies(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + @override + void visitPackageDependencyOverrides(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + @override + void visitPackageDevDependencies(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + @override + void visitPackageDocumentation(PSEntry documentation) { + _checkUrl(documentation.value); + } + + @override + void visitPackageHomepage(PSEntry homepage) { + _checkUrl(homepage.value); + } + + @override + void visitPackageIssueTracker(PSEntry issueTracker) { + _checkUrl(issueTracker.value); + } + + @override + void visitPackageRepository(PSEntry repository) { + _checkUrl(repository.value); + } + + _checkUrl(PSNode? node) { + if (node == null) return; + var text = node.text; + if (text != null) { + var uri = Uri.tryParse(text); + if (uri != null && (uri.isScheme('http') || uri.isScheme('git'))) { + rule.reportPubLint(node, arguments: [uri.scheme]); + } + } + } + + void _visitDeps(PSDependencyList dependencies) { + for (var dep in dependencies) { + _checkUrl(dep.git?.url?.value); + _checkUrl(dep.host?.url?.value); + } + } +} diff --git a/pkg/linter/lib/src/rules/pub/sort_pub_dependencies.dart b/pkg/linter/lib/src/rules/pub/sort_pub_dependencies.dart new file mode 100644 index 00000000000..f5e88870434 --- /dev/null +++ b/pkg/linter/lib/src/rules/pub/sort_pub_dependencies.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/pub.dart'; // ignore: implementation_imports +import 'package:source_span/source_span.dart'; + +import '../../analyzer.dart'; + +const _desc = r'Sort pub dependencies alphabetically.'; + +const _details = r''' +**DO** sort pub dependencies alphabetically (A to Z) in `pubspec.yaml`. + +Sorting list of pub dependencies makes maintenance easier. +'''; + +class SortPubDependencies extends LintRule { + static const LintCode code = LintCode( + 'sort_pub_dependencies', 'Unsorted dependencies.', + correctionMessage: 'Try sorting the dependencies.'); + + SortPubDependencies() + : super( + name: 'sort_pub_dependencies', + description: _desc, + details: _details, + group: Group.pub); + + @override + LintCode get lintCode => code; + + @override + PubspecVisitor getPubspecVisitor() => Visitor(this); +} + +class Visitor extends PubspecVisitor { + final LintRule rule; + + Visitor(this.rule); + + @override + void visitPackageDependencies(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + @override + void visitPackageDependencyOverrides(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + @override + void visitPackageDevDependencies(PSDependencyList dependencies) { + _visitDeps(dependencies); + } + + void _visitDeps(PSDependencyList dependencies) { + int compare(SourceLocation? lc1, SourceLocation? lc2) { + if (lc1 == null || lc2 == null) { + return 0; + } + return lc1.compareTo(lc2); + } + + var depsByLocation = dependencies.toList() + ..sort((d1, d2) => compare(d1.name?.span.start, d2.name?.span.start)); + var previousName = ''; + for (var dep in depsByLocation) { + var name = dep.name; + if (name != null) { + var text = name.text; + if (text != null) { + if (text.compareTo(previousName) < 0) { + rule.reportPubLint(name); + return; + } + previousName = text; + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/public_member_api_docs.dart b/pkg/linter/lib/src/rules/public_member_api_docs.dart new file mode 100644 index 00000000000..1d433d76f60 --- /dev/null +++ b/pkg/linter/lib/src/rules/public_member_api_docs.dart @@ -0,0 +1,324 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; + +const _desc = r'Document all public members.'; + +const _details = r''' +**DO** document all public members. + +All non-overriding public members should be documented with `///` doc-style +comments. + +**BAD:** +```dart +class Bad { + void meh() { } +} +``` + +**GOOD:** +```dart +/// A good thing. +abstract class Good { + /// Start doing your thing. + void start() => _start(); + + _start(); +} +``` + +In case a public member overrides a member it is up to the declaring member +to provide documentation. For example, in the following, `Sub` needn't +document `init` (though it certainly may, if there's need). + +**GOOD:** +```dart +/// Base of all things. +abstract class Base { + /// Initialize the base. + void init(); +} + +/// A sub base. +class Sub extends Base { + @override + void init() { ... } +} +``` + +Note that consistent with `dart doc`, an exception to the rule is made when +documented getters have corresponding undocumented setters. In this case the +setters inherit the docs from the getters. + +'''; + +// TODO(devoncarew): Longer term, this lint could benefit from being more aware +// of the actual API surface area of a package - including that defined by +// exports - and linting against that. + +class PublicMemberApiDocs extends LintRule { + static const LintCode code = LintCode( + 'public_member_api_docs', 'Missing documentation for a public member.', + correctionMessage: 'Try adding documentation for the member.'); + + PublicMemberApiDocs() + : super( + name: 'public_member_api_docs', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!isInLibDir(context.currentUnit.unit, context.package)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + registry.addClassTypeAlias(this, visitor); + registry.addCompilationUnit(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addEnumConstantDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + registry.addExtensionDeclaration(this, visitor); + registry.addExtensionTypeDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + registry.addGenericTypeAlias(this, visitor); + registry.addMixinDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + bool check(Declaration node) { + if (node.documentationComment == null && !isOverridingMember(node)) { + var errorNode = getNodeToAnnotate(node); + rule.reportLintForOffset(errorNode.offset, errorNode.length); + return true; + } + return false; + } + + void checkMethods(List members) { + // Check methods + + var getters = {}; + var setters = []; + + // Non-getters/setters. + var methods = []; + + // Identify getter/setter pairs. + for (var member in members) { + if (member is MethodDeclaration && !isPrivate(member.name)) { + if (member.isGetter) { + getters[member.name.lexeme] = member; + } else if (member.isSetter) { + setters.add(member); + } else { + methods.add(member); + } + } + } + + // Check all getters, and collect offenders along the way. + var missingDocs = {}; + for (var getter in getters.values) { + if (check(getter)) { + missingDocs.add(getter); + } + } + + // But only setters whose getter is missing a doc. + for (var setter in setters) { + var getter = getters[setter.name.lexeme]; + if (getter != null && missingDocs.contains(getter)) { + check(setter); + } + } + + // Check remaining methods. + methods.forEach(check); + } + + /// Whether [node] overrides some other member. + bool isOverridingMember(Declaration node) => + context.inheritanceManager.overriddenMember(node.declaredElement) != null; + + @override + void visitClassDeclaration(ClassDeclaration node) { + var element = node.declaredElement; + if (element == null || element.hasInternal) return; + _visitMembers(node, node.name, node.members); + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + if (!isPrivate(node.name)) { + check(node); + } + } + + @override + void visitCompilationUnit(CompilationUnit node) { + var getters = {}; + var setters = []; + + // Check functions. + + // Non-getters/setters. + var functions = []; + + // Identify getter/setter pairs. + for (var member in node.declarations) { + if (member is FunctionDeclaration) { + var name = member.name; + if (!isPrivate(name) && name.lexeme != 'main') { + if (member.isGetter) { + getters[member.name.lexeme] = member; + } else if (member.isSetter) { + setters.add(member); + } else { + functions.add(member); + } + } + } + } + + // Check all getters, and collect offenders along the way. + var missingDocs = {}; + for (var getter in getters.values) { + if (check(getter)) { + missingDocs.add(getter); + } + } + + // But only setters whose getter is missing a doc. + for (var setter in setters) { + var getter = getters[setter.name.lexeme]; + if (getter != null && missingDocs.contains(getter)) { + check(setter); + } + } + + // Check remaining functions. + functions.forEach(check); + + super.visitCompilationUnit(node); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + if (inPrivateMember(node) || isPrivate(node.name)) return; + var parent = node.parent; + if (parent is EnumDeclaration) return; + if (parent != null && parent.isEffectivelyPrivate) return; + + check(node); + } + + @override + void visitEnumConstantDeclaration(EnumConstantDeclaration node) { + // todo(pq): update this to be called from the parent (like with visitMembers) + if (node.isInternal) return; + + if (!inPrivateMember(node) && !isPrivate(node.name)) { + check(node); + } + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + if (isPrivate(node.name)) return; + if (node.isInternal) return; + + check(node); + checkMethods(node.members); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + if (node.name == null || isPrivate(node.name)) return; + if (node.isInternal) return; + + check(node); + checkMethods(node.members); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + var element = node.declaredElement; + if (element == null || element.hasInternal) return; + _visitMembers(node, node.name, node.members); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + // todo(pq): update this to be called from the parent (like with visitMembers) + if (node.isInternal) return; + if (inPrivateMember(node)) return; + if (node.isInvalidExtensionTypeField) return; + + for (var field in node.fields.variables) { + if (!isPrivate(field.name)) { + check(field); + } + } + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + if (!isPrivate(node.name)) { + check(node); + } + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + if (!isPrivate(node.name)) { + check(node); + } + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + if (node.isInternal) return; + _visitMembers(node, node.name, node.members); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + for (var decl in node.variables.variables) { + if (!isPrivate(decl.name)) { + check(decl); + } + } + } + + void _visitMembers(Declaration node, Token name, List members) { + if (isPrivate(name)) return; + + check(node); + checkMethods(members); + } +} diff --git a/pkg/linter/lib/src/rules/recursive_getters.dart b/pkg/linter/lib/src/rules/recursive_getters.dart new file mode 100644 index 00000000000..e0579e643ee --- /dev/null +++ b/pkg/linter/lib/src/rules/recursive_getters.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Property getter recursively returns itself.'; + +const _details = r''' +**DON'T** create recursive getters. + +Recursive getters are getters which return themselves as a value. This is +usually a typo. + +**BAD:** +```dart +int get field => field; // LINT +``` + +**BAD:** +```dart +int get otherField { + return otherField; // LINT +} +``` + +**GOOD:** +```dart +int get field => _field; +``` + +'''; + +class RecursiveGetters extends LintRule { + static const LintCode code = LintCode( + 'recursive_getters', "The getter '{0}' recursively returns itself.", + correctionMessage: 'Try changing the value being returned.'); + + RecursiveGetters() + : super( + name: 'recursive_getters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + } +} + +class _BodyVisitor extends RecursiveAstVisitor { + final LintRule rule; + final ExecutableElement element; + _BodyVisitor(this.element, this.rule); + + bool isSelfReference(SimpleIdentifier node) { + if (node.staticElement != element) return false; + var parent = node.parent; + if (parent is PrefixedIdentifier) return false; + if (parent is PropertyAccess && parent.target is! ThisExpression) { + return false; + } + return true; + } + + @override + visitListLiteral(ListLiteral node) { + if (node.isConst) return null; + return super.visitListLiteral(node); + } + + @override + visitSetOrMapLiteral(SetOrMapLiteral node) { + if (node.isConst) return null; + return super.visitSetOrMapLiteral(node); + } + + @override + visitSimpleIdentifier(SimpleIdentifier node) { + if (isSelfReference(node)) { + rule.reportLint(node, arguments: [node.name]); + } + + // No need to call super visit (SimpleIdentifiers have no children). + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // getters have null arguments, methods have parameters, could be empty. + if (node.functionExpression.parameters != null) return; + + _verifyElement(node.functionExpression, node.declaredElement); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + // getters have null arguments, methods have parameters, could be empty. + if (node.parameters != null) return; + + _verifyElement(node.body, node.declaredElement); + } + + void _verifyElement(AstNode node, ExecutableElement? element) { + if (element == null) return; + node.accept(_BodyVisitor(element, rule)); + } +} diff --git a/pkg/linter/lib/src/rules/require_trailing_commas.dart b/pkg/linter/lib/src/rules/require_trailing_commas.dart new file mode 100644 index 00000000000..e972bc309aa --- /dev/null +++ b/pkg/linter/lib/src/rules/require_trailing_commas.dart @@ -0,0 +1,214 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use trailing commas for all function calls and declarations.'; + +const _details = r''' +**DO** use trailing commas for all function calls and declarations unless the +function call or definition, from the start of the function name up to the +closing parenthesis, fits in a single line. + +**BAD:** +```dart +void run() { + method('does not fit on one line', + 'test test test test test test test test test test test'); +} +``` + +**GOOD:** +```dart +void run() { + method( + 'does not fit on one line', + 'test test test test test test test test test test test', + ); +} +``` + +**EXCEPTION:** If the final parameter/argument is positional (vs named) and is +either a function literal implemented using curly braces, a literal map, a +literal set or a literal array. This exception only applies if the final +parameter does not fit entirely on one line. + +**NOTE:** This lint rule assumes `dart format` has been run over the code and +may produce false positives until that has happened. + +'''; + +class RequireTrailingCommas extends LintRule { + static const LintCode code = LintCode( + 'require_trailing_commas', 'Missing trailing comma.', + correctionMessage: 'Try adding a trailing comma.'); + + RequireTrailingCommas() + : super( + name: 'require_trailing_commas', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this); + registry + ..addArgumentList(this, visitor) + ..addAssertInitializer(this, visitor) + ..addAssertStatement(this, visitor) + ..addCompilationUnit(this, visitor) + ..addFormalParameterList(this, visitor) + ..addListLiteral(this, visitor) + ..addSetOrMapLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + static const _trailingCommaCode = LintCode( + 'require_trailing_commas', + 'Missing a required trailing comma.', + ); + + final LintRule rule; + + late LineInfo _lineInfo; + + _Visitor(this.rule); + + @override + void visitArgumentList(ArgumentList node) { + super.visitArgumentList(node); + if (node.arguments.isEmpty) return; + _checkTrailingComma( + node.leftParenthesis, + node.rightParenthesis, + node.arguments.last, + ); + } + + @override + void visitAssertInitializer(AssertInitializer node) { + super.visitAssertInitializer(node); + _checkTrailingComma( + node.leftParenthesis, + node.rightParenthesis, + node.message ?? node.condition, + ); + } + + @override + void visitAssertStatement(AssertStatement node) { + super.visitAssertStatement(node); + _checkTrailingComma( + node.leftParenthesis, + node.rightParenthesis, + node.message ?? node.condition, + ); + } + + @override + void visitCompilationUnit(CompilationUnit node) => _lineInfo = node.lineInfo; + + @override + void visitFormalParameterList(FormalParameterList node) { + super.visitFormalParameterList(node); + if (node.parameters.isEmpty) return; + _checkTrailingComma( + node.leftParenthesis, + node.rightParenthesis, + node.parameters.last, + ); + } + + @override + void visitListLiteral(ListLiteral node) { + super.visitListLiteral(node); + if (node.elements.isNotEmpty) { + _checkTrailingComma( + node.leftBracket, + node.rightBracket, + node.elements.last, + ); + } + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + super.visitSetOrMapLiteral(node); + if (node.elements.isNotEmpty) { + _checkTrailingComma( + node.leftBracket, + node.rightBracket, + node.elements.last, + ); + } + } + + void _checkTrailingComma( + Token leftParenthesis, + Token rightParenthesis, + AstNode lastNode, + ) { + // Early exit if trailing comma is present. + if (lastNode.endToken.next?.type == TokenType.COMMA) return; + + // No trailing comma is needed if the function call or declaration, up to + // the closing parenthesis, fits on a single line. Ensuring the left and + // right parenthesis are on the same line is sufficient since `dart format` + // places the left parenthesis right after the identifier (on the same + // line). + if (_isSameLine(leftParenthesis, rightParenthesis)) return; + + // Check the last parameter to determine if there are any exceptions. + if (_shouldAllowTrailingCommaException(lastNode)) return; + + rule.reportLintForToken(rightParenthesis, errorCode: _trailingCommaCode); + } + + bool _isSameLine(Token token1, Token token2) => + _lineInfo.getLocation(token1.offset).lineNumber == + _lineInfo.getLocation(token2.end).lineNumber; + + bool _shouldAllowTrailingCommaException(AstNode lastNode) { + // No exceptions are allowed if the last argument is named. + if (lastNode is FormalParameter && lastNode.isNamed) return false; + + // No exceptions are allowed if the entire last argument fits on one line. + if (_isSameLine(lastNode.beginToken, lastNode.endToken)) return false; + + // Exception is allowed if the last argument is a function literal. + if (lastNode is FunctionExpression && lastNode.body is BlockFunctionBody) { + return true; + } + + // Exception is allowed if the last argument is a (multiline) string literal. + if (lastNode is StringLiteral) return true; + + // Exception is allowed if the last argument is a anonymous function call. + // This case arises a lot in asserts. + if (lastNode is FunctionExpressionInvocation && + lastNode.function is FunctionExpression && + _isSameLine(lastNode.argumentList.leftParenthesis, + lastNode.argumentList.rightParenthesis)) { + return true; + } + + // Exception is allowed if the last argument is a set, map or list literal. + if (lastNode is SetOrMapLiteral || lastNode is ListLiteral) return true; + + return false; + } +} diff --git a/pkg/linter/lib/src/rules/sized_box_for_whitespace.dart b/pkg/linter/lib/src/rules/sized_box_for_whitespace.dart new file mode 100644 index 00000000000..771c006742a --- /dev/null +++ b/pkg/linter/lib/src/rules/sized_box_for_whitespace.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'SizedBox for whitespace.'; + +const _details = r''' +Use SizedBox to add whitespace to a layout. + +A `Container` is a heavier Widget than a `SizedBox`, and as bonus, `SizedBox` +has a `const` constructor. + +**BAD:** +```dart +Widget buildRow() { + return Row( + children: [ + const MyLogo(), + Container(width: 4), + const Expanded( + child: Text('...'), + ), + ], + ); +} +``` + +**GOOD:** +```dart +Widget buildRow() { + return Row( + children: const [ + MyLogo(), + SizedBox(width: 4), + Expanded( + child: Text('...'), + ), + ], + ); +} +``` +'''; + +class SizedBoxForWhitespace extends LintRule { + static const LintCode code = LintCode('sized_box_for_whitespace', + "Use a 'SizedBox' to add whitespace to a layout.", + correctionMessage: "Try using a 'SizedBox' rather than a 'Container'."); + + SizedBoxForWhitespace() + : super( + name: 'sized_box_for_whitespace', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _ArgumentData { + var incompatibleParamsFound = false; + + var positionalArgumentFound = false; + var seenWidth = false; + var seenHeight = false; + var seenChild = false; + _ArgumentData(ArgumentList node) { + for (var argument in node.arguments) { + if (argument is! NamedExpression) { + positionalArgumentFound = true; + return; + } + var label = argument.name.label; + if (label.name == 'width') { + seenWidth = true; + } else if (label.name == 'height') { + seenHeight = true; + } else if (label.name == 'child') { + seenChild = true; + } else if (label.name == 'key') { + // key doesn't matter (both SizedBox and Container have it) + } else { + incompatibleParamsFound = true; + } + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isExactWidgetTypeContainer(node.staticType)) { + return; + } + + var data = _ArgumentData(node.argumentList); + + if (data.incompatibleParamsFound || data.positionalArgumentFound) { + return; + } + if (data.seenChild && (data.seenWidth || data.seenHeight) || + data.seenWidth && data.seenHeight) { + rule.reportLint(node.constructorName); + } + } +} diff --git a/pkg/linter/lib/src/rules/sized_box_shrink_expand.dart b/pkg/linter/lib/src/rules/sized_box_shrink_expand.dart new file mode 100644 index 00000000000..a1eacaaa34e --- /dev/null +++ b/pkg/linter/lib/src/rules/sized_box_shrink_expand.dart @@ -0,0 +1,145 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _details = + r'''Use `SizedBox.shrink(...)` and `SizedBox.expand(...)` constructors appropriately. + +The `SizedBox.shrink(...)` and `SizedBox.expand(...)` constructors should be used +instead of the more general `SizedBox(...)` constructor when the named constructors +capture the intent of the code more succinctly. + +**Examples** + +**BAD:** +```dart +Widget buildLogo() { + return SizedBox( + height: 0, + width: 0, + child: const MyLogo(), + ); +} +``` + +```dart +Widget buildLogo() { + return SizedBox( + height: double.infinity, + width: double.infinity, + child: const MyLogo(), + ); +} +``` + +**GOOD:** +```dart +Widget buildLogo() { + return SizedBox.shrink( + child: const MyLogo(), + ); +} +``` + +```dart +Widget buildLogo() { + return SizedBox.expand( + child: const MyLogo(), + ); +} +``` +'''; + +class SizedBoxShrinkExpand extends LintRule { + static const LintCode code = LintCode( + 'sized_box_shrink_expand', + "Use 'SizedBox.{0}' to avoid needing to specify the 'height' and " + "'width'.", + correctionMessage: + "Try using 'SizedBox.{0}' and removing the 'height' and 'width' " + 'arguments.'); + + SizedBoxShrinkExpand() + : super( + name: 'sized_box_shrink_expand', + description: 'Use SizedBox shrink and expand named constructors.', + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _ArgumentData { + var positionalArgumentFound = false; + + double? width; + + double? height; + _ArgumentData(ArgumentList node) { + for (var argument in node.arguments) { + if (argument is! NamedExpression) { + positionalArgumentFound = true; + return; + } + var label = argument.name.label; + if (label.name == 'width') { + width = _argumentValue(argument.expression); + } else if (label.name == 'height') { + height = _argumentValue(argument.expression); + } + } + } + double? _argumentValue(Expression argument) { + if (argument is IntegerLiteral) { + return argument.value?.toDouble(); + } else if (argument is DoubleLiteral) { + return argument.value; + } else if (argument is PrefixedIdentifier && + argument.identifier.name == 'infinity' && + argument.prefix.name == 'double') { + return double.infinity; + } + return null; + } +} + +class _Visitor extends SimpleAstVisitor { + final SizedBoxShrinkExpand rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + // Only interested in the default constructor for the SizedBox widget + if (!isExactWidgetTypeSizedBox(node.staticType) || + node.constructorName.name != null) { + return; + } + + var data = _ArgumentData(node.argumentList); + if (data.positionalArgumentFound) { + return; + } + if (data.width == 0 && data.height == 0) { + rule.reportLint(node.constructorName, arguments: ['shrink']); + } else if (data.width == double.infinity && + data.height == double.infinity) { + rule.reportLint(node.constructorName, arguments: ['expand']); + } + } +} diff --git a/pkg/linter/lib/src/rules/slash_for_doc_comments.dart b/pkg/linter/lib/src/rules/slash_for_doc_comments.dart new file mode 100644 index 00000000000..990cb89f3b4 --- /dev/null +++ b/pkg/linter/lib/src/rules/slash_for_doc_comments.dart @@ -0,0 +1,176 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Prefer using /// for doc comments.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/documentation#do-use--doc-comments-to-document-members-and-types): + +**DO** use `///` for documentation comments. + +Although Dart supports two syntaxes of doc comments (`///` and `/**`), we +prefer using `///` for doc comments. + +**GOOD:** +```dart +/// Parses a set of option strings. For each option: +/// +/// * If it is `null`, then it is ignored. +/// * If it is a string, then [validate] is called on it. +/// * If it is any other type, it is *not* validated. +void parse(List options) { + // ... +} +``` + +Within a doc comment, you can use markdown for formatting. + +'''; + +bool isJavaStyle(Comment comment) { + var tokens = comment.tokens; + if (tokens.isEmpty) { + return false; + } + //Should be only one + return comment.tokens.first.lexeme.startsWith('/**'); +} + +class SlashForDocComments extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode('slash_for_doc_comments', + "Use the end-of-line form ('///') for doc comments.", + correctionMessage: "Try rewriting the comment to use '///'."); + + SlashForDocComments() + : super( + name: 'slash_for_doc_comments', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addClassTypeAlias(this, visitor); + registry.addCompilationUnit(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addEnumConstantDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + registry.addExtensionDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addFunctionDeclarationStatement(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + registry.addGenericTypeAlias(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addMixinDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void checkComment(Comment? comment) { + if (comment != null && isJavaStyle(comment)) { + rule.reportLint(comment); + } + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitClassTypeAlias(ClassTypeAlias node) { + checkComment(node.documentationComment); + } + + @override + void visitCompilationUnit(CompilationUnit node) { + var directives = node.directives; + if (directives.isNotEmpty) { + checkComment(directives.first.documentationComment); + } + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitEnumConstantDeclaration(EnumConstantDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { + var comment = node.beginToken.precedingComments; + if (comment != null && comment.lexeme.startsWith('/**')) { + rule.reportLintForToken(comment); + } + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + checkComment(node.documentationComment); + } + + @override + void visitGenericTypeAlias(GenericTypeAlias node) { + checkComment(node.documentationComment); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + checkComment(node.documentationComment); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + checkComment(node.documentationComment); + } +} diff --git a/pkg/linter/lib/src/rules/sort_child_properties_last.dart b/pkg/linter/lib/src/rules/sort_child_properties_last.dart new file mode 100644 index 00000000000..e4a3ef42145 --- /dev/null +++ b/pkg/linter/lib/src/rules/sort_child_properties_last.dart @@ -0,0 +1,148 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Sort child properties last in widget instance creations.'; + +const _details = r''' +Sort child properties last in widget instance creations. This improves +readability and plays nicest with UI as Code visualization in IDEs with UI as +Code Guides in editors (such as IntelliJ) where Properties in the correct order +appear clearly associated with the constructor call and separated from the +children. + +**BAD:** +```dart +return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.display1, + ), + ], + mainAxisAlignment: MainAxisAlignment.center, + ), + widthFactor: 0.5, + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + onPressed: _incrementCounter, + tooltip: 'Increment', + ), +); +``` + +**GOOD:** +```dart +return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + widthFactor: 0.5, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.display1, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), +); +``` + +Exception: It's allowed to have parameter with a function expression after the +`child` property. + +'''; + +class SortChildPropertiesLast extends LintRule { + static const LintCode code = LintCode('sort_child_properties_last', + "The '{0}' argument should be last in widget constructor invocations.", + correctionMessage: + 'Try moving the argument to the end of the argument list.'); + + SortChildPropertiesLast() + : super( + name: 'sort_child_properties_last', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isWidgetType(node.staticType)) { + return; + } + + var arguments = node.argumentList.arguments; + if (arguments.length < 2 || + isChildArg(arguments.last) || + arguments.where(isChildArg).length != 1) { + return; + } + + var onlyClosuresAfterChild = arguments.reversed + .takeWhile((argument) => !isChildArg(argument)) + .toList() + .reversed + .where((element) => + element is NamedExpression && + element.expression is! FunctionExpression) + .isEmpty; + if (!onlyClosuresAfterChild) { + var argument = arguments.firstWhere(isChildArg); + var name = (argument as NamedExpression).name.label.name; + rule.reportLint(argument, arguments: [name]); + } + } + + static bool isChildArg(Expression e) { + if (e is NamedExpression) { + var name = e.name.label.name; + return (name == 'child' || name == 'children') && + isWidgetProperty(e.staticType); + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/sort_constructors_first.dart b/pkg/linter/lib/src/rules/sort_constructors_first.dart new file mode 100644 index 00000000000..aaa2813d2e4 --- /dev/null +++ b/pkg/linter/lib/src/rules/sort_constructors_first.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Sort constructor declarations before other members.'; + +const _details = r''' +**DO** sort constructor declarations before other members. + +**BAD:** +```dart +abstract class Visitor { + double value; + visitSomething(Something s); + Visitor(); +} +``` + +**GOOD:** +```dart +abstract class Animation { + const Animation(this.value); + double value; + void addListener(VoidCallback listener); +} +``` + +'''; + +class SortConstructorsFirst extends LintRule { + static const LintCode code = LintCode('sort_constructors_first', + 'Constructor declarations should be before non-constructor declarations.', + correctionMessage: + 'Try moving the constructor declaration before all other members.'); + + SortConstructorsFirst() + : super( + name: 'sort_constructors_first', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + registry.addExtensionTypeDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void check(NodeList members) { + var other = false; + // Members are sorted by source position in the AST. + for (var member in members) { + if (member is ConstructorDeclaration) { + if (other) { + rule.reportLint(member.returnType); + } + } else { + other = true; + } + } + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + check(node.members); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + check(node.members); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + check(node.members); + } +} diff --git a/pkg/linter/lib/src/rules/sort_unnamed_constructors_first.dart b/pkg/linter/lib/src/rules/sort_unnamed_constructors_first.dart new file mode 100644 index 00000000000..bd5f1ef4bba --- /dev/null +++ b/pkg/linter/lib/src/rules/sort_unnamed_constructors_first.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Sort unnamed constructor declarations first.'; + +const _details = r''' +**DO** sort unnamed constructor declarations first, before named ones. + +**BAD:** +```dart +class _PriorityItem { + factory _PriorityItem.forName(bool isStatic, String name, _MemberKind kind) => ... + _PriorityItem(this.isStatic, this.kind, this.isPrivate); + ... +} +``` + +**GOOD:** +```dart +abstract class CancelableFuture implements Future { + factory CancelableFuture(computation()) => ... + factory CancelableFuture.delayed(Duration duration, [computation()]) => ... + ... +} +``` + +'''; + +class SortUnnamedConstructorsFirst extends LintRule { + static const LintCode code = LintCode('sort_unnamed_constructors_first', + 'Invalid location for the unnamed constructor.', + correctionMessage: + 'Try moving the unnamed constructor before all other constructors.'); + + SortUnnamedConstructorsFirst() + : super( + name: 'sort_unnamed_constructors_first', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addEnumDeclaration(this, visitor); + registry.addExtensionTypeDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + void check(NodeList members) { + var seenConstructor = false; + // Members are sorted by source position in the AST. + for (var member in members) { + if (member is ConstructorDeclaration) { + if (member.name == null) { + if (seenConstructor) { + rule.reportLint(member.returnType); + } + } else { + seenConstructor = true; + } + } + } + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + check(node.members); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + check(node.members); + } + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { + if (node.representation.constructorName == null) return; + check(node.members); + } +} diff --git a/pkg/linter/lib/src/rules/super_goes_last.dart b/pkg/linter/lib/src/rules/super_goes_last.dart new file mode 100644 index 00000000000..c8bc4b97728 --- /dev/null +++ b/pkg/linter/lib/src/rules/super_goes_last.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../analyzer.dart'; + +const _desc = + r'Place the `super` call last in a constructor initialization list.'; + +const _details = r''' +**DO** place the `super` call last in a constructor initialization list. + +Field initializers are evaluated in the order that they appear in the +constructor initialization list. If you place a `super()` call in the middle of +an initializer list, the superclass's initializers will be evaluated right then +before evaluating the rest of the subclass's initializers. + +What it doesn't mean is that the superclass's constructor body will be executed +then. That always happens after all initializers are run regardless of where +`super` appears. It's vanishingly rare that the order of initializers matters, +so the placement of `super` in the list almost never matters either. + +Getting in the habit of placing it last improves consistency, visually +reinforces when the superclass's constructor body is run, and may help +performance. + +**BAD:** +```dart +View(Style style, List children) + : super(style), + _children = children { +``` + +**GOOD:** +```dart +View(Style style, List children) + : _children = children, + super(style) { +``` +'''; + +class SuperGoesLast extends LintRule { + static const LintCode code = LintCode('super_goes_last', + "Place the 'super' call last in an initialization list.", + correctionMessage: + "Try moving the 'super' invocation to the end of the initialization " + 'list.'); + + SuperGoesLast() + : super( + name: 'super_goes_last', + description: _desc, + details: _details, + state: State.removed(since: dart3), + group: Group.style); + + @override + LintCode get lintCode => code; +} diff --git a/pkg/linter/lib/src/rules/test_types_in_equals.dart b/pkg/linter/lib/src/rules/test_types_in_equals.dart new file mode 100644 index 00000000000..1d2c43db677 --- /dev/null +++ b/pkg/linter/lib/src/rules/test_types_in_equals.dart @@ -0,0 +1,133 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Test type arguments in operator ==(Object other).'; + +const _details = r''' +**DO** test type arguments in operator ==(Object other). + +Not testing types might result in null pointer exceptions which will be +unexpected for consumers of your class. + +**BAD:** +```dart +class Field { +} + +class Bad { + final Field someField; + + Bad(this.someField); + + @override + bool operator ==(Object other) { + Bad otherBad = other as Bad; // LINT + bool areEqual = otherBad != null && otherBad.someField == someField; + return areEqual; + } + + @override + int get hashCode { + return someField.hashCode; + } +} +``` + +**GOOD:** +```dart +class Field { +} + +class Good { + final Field someField; + + Good(this.someField); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is Good && + this.someField == other.someField; + } + + @override + int get hashCode { + return someField.hashCode; + } +} +``` + +'''; + +class TestTypesInEquals extends LintRule { + static const LintCode code = LintCode( + 'test_types_in_equals', "Missing type test for '{0}' in '=='.", + correctionMessage: "Try testing the type of '{0}'."); + + TestTypesInEquals() + : super( + name: 'test_types_in_equals', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAsExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAsExpression(AsExpression node) { + var declaration = node.thisOrAncestorOfType(); + var expression = node.expression; + if (!_isEqualsOverride(declaration) || expression is! SimpleIdentifier) { + return; + } + + var parameters = declaration?.parameters; + var parameterName = parameters?.parameterElements.first?.name; + if (expression.name == parameterName) { + var typeName = _getTypeName(declaration!); + rule.reportLint(node, arguments: [typeName]); + } + } + + String _getTypeName(MethodDeclaration method) { + var parent = method.parent; + if (parent is ClassDeclaration) { + return parent.name.lexeme; + } else if (parent is EnumDeclaration) { + return parent.name.lexeme; + } else if (parent is MixinDeclaration) { + return parent.name.lexeme; + } else if (parent is ExtensionDeclaration) { + return parent.extendedType.toSource(); + } + return 'unknown'; + } + + bool _isEqualsOverride(MethodDeclaration? declaration) => + declaration != null && + declaration.isOperator && + declaration.name.lexeme == '==' && + declaration.parameters?.parameterElements.length == 1; +} diff --git a/pkg/linter/lib/src/rules/throw_in_finally.dart b/pkg/linter/lib/src/rules/throw_in_finally.dart new file mode 100755 index 00000000000..0c7aa532f52 --- /dev/null +++ b/pkg/linter/lib/src/rules/throw_in_finally.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../rules/control_flow_in_finally.dart'; + +const _desc = r'Avoid `throw` in finally block.'; + +const _details = r''' +**AVOID** throwing exceptions in finally blocks. + +Throwing exceptions in finally blocks will inevitably cause unexpected behavior +that is hard to debug. + +**BAD:** +```dart +class BadThrow { + double nonCompliantMethod() { + try { + print('hello world! ${1 / 0}'); + } catch (e) { + print(e); + } finally { + throw 'Find the hidden error :P'; // LINT + } + } +} +``` + +**GOOD:** +```dart +class Ok { + double compliantMethod() { + var i = 5; + try { + i = 1 / 0; + } catch (e) { + print(e); // OK + } + return i; + } +} +``` + +'''; + +class ThrowInFinally extends LintRule { + static const LintCode code = LintCode( + 'throw_in_finally', "Use of '{0}' in 'finally' block.", + correctionMessage: "Try moving the '{0}' outside the 'finally' block."); + + ThrowInFinally() + : super( + name: 'throw_in_finally', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addThrowExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor + with ControlFlowInFinallyBlockReporter { + @override + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitThrowExpression(ThrowExpression node) { + reportIfFinallyAncestorExists(node, kind: 'throw'); + } +} diff --git a/pkg/linter/lib/src/rules/tighten_type_of_initializing_formals.dart b/pkg/linter/lib/src/rules/tighten_type_of_initializing_formals.dart new file mode 100644 index 00000000000..d22aaac7918 --- /dev/null +++ b/pkg/linter/lib/src/rules/tighten_type_of_initializing_formals.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Tighten type of initializing formal.'; + +const _details = r''' +Tighten the type of an initializing formal if a non-null assert exists. This +allows the type system to catch problems rather than have them only be caught at +run-time. + +**BAD:** +```dart +class A { + A.c1(this.p) : assert(p != null); + A.c2(this.p); + final String? p; +} +``` + +**GOOD:** +```dart +class A { + A.c1(String this.p); + A.c2(this.p); + final String? p; +} + +class B { + String? b; + B(this.b); +} + +class C extends B { + B(String super.b); +} +``` +'''; + +class TightenTypeOfInitializingFormals extends LintRule { + static const LintCode code = LintCode('tighten_type_of_initializing_formals', + "Use a type annotation rather than 'assert' to enforce non-nullability.", + correctionMessage: + "Try adding a type annotation and removing the 'assert'."); + + TightenTypeOfInitializingFormals() + : super( + name: 'tighten_type_of_initializing_formals', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + for (var initializer in node.initializers) { + if (initializer is! AssertInitializer) continue; + + var condition = initializer.condition; + if (condition is! BinaryExpression) continue; + + if (condition.operator.type == TokenType.BANG_EQ) { + if (condition.rightOperand is NullLiteral) { + var leftOperand = condition.leftOperand; + if (leftOperand is Identifier) { + var staticType = leftOperand.staticType; + if (staticType != null && + context.typeSystem.isNullable(staticType)) { + _check(leftOperand.staticElement, node); + } + } + } else if (condition.leftOperand is NullLiteral) { + var rightOperand = condition.rightOperand; + if (rightOperand is Identifier) { + var staticType = rightOperand.staticType; + if (staticType != null && + context.typeSystem.isNullable(staticType)) { + _check(rightOperand.staticElement, node); + } + } + } + } + } + } + + void _check(Element? element, ConstructorDeclaration node) { + if (element is FieldFormalParameterElement || + element is SuperFormalParameterElement) { + rule.reportLint(node.parameters.parameters + .firstWhere((p) => p.declaredElement == element)); + } + } +} diff --git a/pkg/linter/lib/src/rules/type_annotate_public_apis.dart b/pkg/linter/lib/src/rules/type_annotate_public_apis.dart new file mode 100644 index 00000000000..a51c4060348 --- /dev/null +++ b/pkg/linter/lib/src/rules/type_annotate_public_apis.dart @@ -0,0 +1,165 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../util/ascii_utils.dart'; + +const _desc = r'Type annotate public APIs.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#do-type-annotate-fields-and-top-level-variables-if-the-type-isnt-obvious): + +**PREFER** type annotating public APIs. + +Type annotations are important documentation for how a library should be used. +Annotating the parameter and return types of public methods and functions helps +users understand what the API expects and what it provides. + +Note that if a public API accepts a range of values that Dart's type system +cannot express, then it is acceptable to leave that untyped. In that case, the +implicit `dynamic` is the correct type for the API. + +For code internal to a library (either private, or things like nested functions) +annotate where you feel it helps, but don't feel that you *must* provide them. + +**BAD:** +```dart +install(id, destination) { + // ... +} +``` + +Here, it's unclear what `id` is. A string? And what is `destination`? A string +or a `File` object? Is this method synchronous or asynchronous? + +**GOOD:** +```dart +Future install(PackageId id, String destination) { + // ... +} +``` + +With types, all of this is clarified. + +'''; + +class TypeAnnotatePublicApis extends LintRule { + static const LintCode code = LintCode( + 'type_annotate_public_apis', 'Missing type annotation on a public API.', + correctionMessage: 'Try adding a type annotation.'); + + TypeAnnotatePublicApis() + : super( + name: 'type_annotate_public_apis', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + registry.addFunctionTypeAlias(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final _VisitorHelper v; + + _Visitor(this.rule) : v = _VisitorHelper(rule); + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.fields.type == null) { + node.fields.accept(v); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + if (!isPrivate(node.name) && + // Only report on top-level functions, not those declared within the + // scope of another function. + node.parent is CompilationUnit) { + if (node.returnType == null && !node.isSetter) { + rule.reportLintForToken(node.name); + } else { + node.functionExpression.parameters?.accept(v); + } + } + } + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) { + if (!isPrivate(node.name)) { + if (node.returnType == null) { + rule.reportLintForToken(node.name); + } else { + node.parameters.accept(v); + } + } + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (!isPrivate(node.name)) { + if (node.returnType == null && !node.isSetter) { + rule.reportLintForToken(node.name); + } else { + node.parameters?.accept(v); + } + } + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + if (node.variables.type == null) { + node.variables.accept(v); + } + } +} + +class _VisitorHelper extends RecursiveAstVisitor { + final LintRule rule; + + _VisitorHelper(this.rule); + + bool hasInferredType(VariableDeclaration node) { + var staticType = node.initializer?.staticType; + return staticType != null && + staticType is! DynamicType && + !staticType.isDartCoreNull; + } + + @override + void visitSimpleFormalParameter(SimpleFormalParameter param) { + if (param.type == null) { + var paramName = param.name?.lexeme; + if (paramName != null && !paramName.isJustUnderscores) { + rule.reportLint(param); + } + } + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (!isPrivate(node.name) && + !node.isConst && + !(node.isFinal && hasInferredType(node))) { + rule.reportLintForToken(node.name); + } + } +} diff --git a/pkg/linter/lib/src/rules/type_init_formals.dart b/pkg/linter/lib/src/rules/type_init_formals.dart new file mode 100644 index 00000000000..ee1dcd1bb61 --- /dev/null +++ b/pkg/linter/lib/src/rules/type_init_formals.dart @@ -0,0 +1,128 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = "Don't type annotate initializing formals."; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#dont-type-annotate-initializing-formals): + +**DON'T** type annotate initializing formals. + +If a constructor parameter is using `this.x` to initialize a field, then the +type of the parameter is understood to be the same type as the field. If a +a constructor parameter is using `super.x` to forward to a super constructor, +then the type of the parameter is understood to be the same as the super +constructor parameter. + +Type annotating an initializing formal with a different type than that of the +field is OK. + +**BAD:** +```dart +class Point { + int x, y; + Point(int this.x, int this.y); +} +``` + +**GOOD:** +```dart +class Point { + int x, y; + Point(this.x, this.y); +} +``` + +**BAD:** +```dart +class A { + int a; + A(this.a); +} + +class B extends A { + B(int super.a); +} +``` + +**GOOD:** +```dart +class A { + int a; + A(this.a); +} + +class B extends A { + B(super.a); +} +``` + +'''; + +class TypeInitFormals extends LintRule { + static const LintCode code = LintCode('type_init_formals', + "Don't needlessly type annotate initializing formals.", + correctionMessage: 'Try removing the type.'); + + TypeInitFormals() + : super( + name: 'type_init_formals', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldFormalParameter(this, visitor); + registry.addSuperFormalParameter(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFieldFormalParameter(FieldFormalParameter node) { + var nodeType = node.type; + if (nodeType == null) return; + + var paramElement = node.declaredElement; + if (paramElement is! FieldFormalParameterElement) return; + + var field = paramElement.field; + // If no such field exists, the code is invalid; do not report lint. + if (field != null && nodeType.type == field.type) { + rule.reportLint(nodeType); + } + } + + @override + void visitSuperFormalParameter(SuperFormalParameter node) { + var nodeType = node.type; + if (nodeType == null) return; + + var paramElement = node.declaredElement; + if (paramElement is! SuperFormalParameterElement) return; + + var superConstructorParameter = paramElement.superConstructorParameter; + if (superConstructorParameter == null) return; + + if (superConstructorParameter.type == nodeType.type) { + rule.reportLint(nodeType); + } + } +} diff --git a/pkg/linter/lib/src/rules/type_literal_in_constant_pattern.dart b/pkg/linter/lib/src/rules/type_literal_in_constant_pattern.dart new file mode 100644 index 00000000000..73766061fa9 --- /dev/null +++ b/pkg/linter/lib/src/rules/type_literal_in_constant_pattern.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't use constant patterns with type literals."; + +const _details = r''' +If you meant to test if the object has type `Foo`, instead write `Foo _`. + +**BAD:** +```dart +void f(Object? x) { + if (x case num) { + print('int or double'); + } +} +``` + +**GOOD:** +```dart +void f(Object? x) { + if (x case num _) { + print('int or double'); + } +} +``` + +If you do mean to test that the matched value (which you expect to have the +type `Type`) is equal to the type literal `Foo`, then this lint can be +silenced using `const (Foo)`. + +**BAD:** +```dart +void f(Object? x) { + if (x case int) { + print('int'); + } +} +``` + +**GOOD:** +```dart +void f(Object? x) { + if (x case const (int)) { + print('int'); + } +} +``` +'''; + +class TypeLiteralInConstantPattern extends LintRule { + static const String lintName = 'type_literal_in_constant_pattern'; + + static const LintCode code = LintCode( + lintName, + "Use 'TypeName _' instead of a type literal.", + correctionMessage: "Replace with 'TypeName _'.", + ); + + TypeLiteralInConstantPattern() + : super( + name: lintName, + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addConstantPattern(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + visitConstantPattern(ConstantPattern node) { + // `const (MyType)` is fine. + if (node.constKeyword != null) { + return; + } + + var expressionType = node.expression.staticType; + if (expressionType != null && expressionType.isDartCoreType) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unawaited_futures.dart b/pkg/linter/lib/src/rules/unawaited_futures.dart new file mode 100644 index 00000000000..1bbce6f827b --- /dev/null +++ b/pkg/linter/lib/src/rules/unawaited_futures.dart @@ -0,0 +1,142 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'`Future` results in `async` function bodies must be ' + '`await`ed or marked `unawaited` using `dart:async`.'; + +const _details = r''' +**DO** await functions that return a `Future` inside of an async function body. + +It's easy to forget await in async methods as naming conventions usually don't +tell us if a method is sync or async (except for some in `dart:io`). + +When you really _do_ want to start a fire-and-forget `Future`, the recommended +way is to use `unawaited` from `dart:async`. The `// ignore` and +`// ignore_for_file` comments also work. + +**BAD:** +```dart +void main() async { + doSomething(); // Likely a bug. +} +``` + +**GOOD:** +```dart +Future doSomething() => ...; + +void main() async { + await doSomething(); + + unawaited(doSomething()); // Explicitly-ignored fire-and-forget. +} +``` + +'''; + +class UnawaitedFutures extends LintRule { + static const LintCode code = LintCode('unawaited_futures', + "Missing an 'await' for the 'Future' computed by this expression.", + correctionMessage: + "Try adding an 'await' or wrapping the expression un 'unawaited'."); + + UnawaitedFutures() + : super( + name: 'unawaited_futures', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addExpressionStatement(this, visitor); + registry.addCascadeExpression(this, visitor); + registry.addInterpolationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitCascadeExpression(CascadeExpression node) { + var sections = node.cascadeSections; + for (var i = 0; i < sections.length; i++) { + _visit(sections[i]); + } + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + var expr = node.expression; + if (expr is AssignmentExpression) return; + + var type = expr.staticType; + if (type == null) { + return; + } + if (type.implementsInterface('Future', 'dart.async')) { + // Ignore a couple of special known cases. + if (_isFutureDelayedInstanceCreationWithComputation(expr) || + _isMapPutIfAbsentInvocation(expr)) { + return; + } + + if (_isEnclosedInAsyncFunctionBody(node)) { + // Future expression statement that isn't awaited in an async function: + // while this is legal, it's a very frequent sign of an error. + rule.reportLint(node); + } + } + } + + @override + void visitInterpolationExpression(InterpolationExpression node) { + _visit(node.expression); + } + + bool _isEnclosedInAsyncFunctionBody(AstNode node) { + var enclosingFunctionBody = node.thisOrAncestorOfType(); + return enclosingFunctionBody?.isAsynchronous ?? false; + } + + /// Detects `Future.delayed(duration, [computation])` creations with a + /// computation. + bool _isFutureDelayedInstanceCreationWithComputation(Expression expr) => + expr is InstanceCreationExpression && + (expr.staticType?.isDartAsyncFuture ?? false) && + expr.constructorName.name?.name == 'delayed' && + expr.argumentList.arguments.length == 2; + + bool _isMapClass(Element? e) => + e is ClassElement && e.name == 'Map' && e.library.name == 'dart.core'; + + /// Detects Map.putIfAbsent invocations. + bool _isMapPutIfAbsentInvocation(Expression expr) => + expr is MethodInvocation && + expr.methodName.name == 'putIfAbsent' && + _isMapClass(expr.methodName.staticElement?.enclosingElement); + + void _visit(Expression expr) { + if ((expr.staticType?.isDartAsyncFuture ?? false) && + _isEnclosedInAsyncFunctionBody(expr) && + expr is! AssignmentExpression) { + rule.reportLint(expr); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_await_in_return.dart b/pkg/linter/lib/src/rules/unnecessary_await_in_return.dart new file mode 100644 index 00000000000..d1059e4c829 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_await_in_return.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary await keyword in return.'; + +const _details = r''' +Avoid returning an awaited expression when the expression type is assignable to +the function's return type. + + +**BAD:** +```dart +Future future; +Future f1() async => await future; +Future f2() async { + return await future; +} +``` + +**GOOD:** +```dart +Future future; +Future f1() => future; +Future f2() { + return future; +} +``` + +'''; + +class UnnecessaryAwaitInReturn extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_await_in_return', "Unnecessary 'await'.", + correctionMessage: "Try removing the 'await'."); + + UnnecessaryAwaitInReturn() + : super( + name: 'unnecessary_await_in_return', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context.typeSystem); + registry.addExpressionFunctionBody(this, visitor); + registry.addReturnStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final TypeSystem typeSystem; + + _Visitor(this.rule, this.typeSystem); + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _visit(node, node.expression.unParenthesized); + } + + @override + void visitReturnStatement(ReturnStatement node) { + var expression = node.expression; + if (expression != null) { + _visit(node, expression.unParenthesized); + } + } + + void _visit(AstNode node, Expression expression) { + if (expression is! AwaitExpression) return; + + var type = expression.expression.staticType; + if (type?.isDartAsyncFuture != true) { + return; + } + + var parent = node.thisOrAncestorMatching((e) => + e is FunctionExpression || + e is MethodDeclaration || + e is Block && e.parent is TryStatement); + if (parent == null) return; + + DartType? returnType; + if (parent is FunctionExpression) { + returnType = parent.declaredElement?.returnType; + } else if (parent is MethodDeclaration) { + returnType = parent.declaredElement?.returnType; + } else if (parent is Block) { + // removing await in try block changes the behaviour + return; + } else { + throw StateError('unexpected type'); + } + if (returnType != null && + returnType.isDartAsyncFuture && + typeSystem.isSubtypeOf(type!, returnType)) { + rule.reportLintForToken(expression.awaitKeyword); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_brace_in_string_interps.dart b/pkg/linter/lib/src/rules/unnecessary_brace_in_string_interps.dart new file mode 100644 index 00000000000..7b3cd917456 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_brace_in_string_interps.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid using braces in interpolation when not needed.'; + +const _details = r''' +**AVOID** using braces in interpolation when not needed. + +If you're just interpolating a simple identifier, and it's not immediately +followed by more alphanumeric text, the `{}` can and should be omitted. + +**BAD:** +```dart +print("Hi, ${name}!"); +``` + +**GOOD:** +```dart +print("Hi, $name!"); +``` + +'''; + +final RegExp identifierPart = RegExp('[a-zA-Z0-9_]'); + +bool isIdentifierPart(Token? token) => + token is StringToken && token.lexeme.startsWith(identifierPart); + +class UnnecessaryBraceInStringInterps extends LintRule { + static const LintCode code = LintCode('unnecessary_brace_in_string_interps', + 'Unnecessary braces in a string interpolation.', + correctionMessage: 'Try removing the braces.'); + + UnnecessaryBraceInStringInterps() + : super( + name: 'unnecessary_brace_in_string_interps', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addStringInterpolation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitStringInterpolation(StringInterpolation node) { + var expressions = node.elements.whereType(); + for (var expression in expressions) { + var exp = expression.expression; + if (exp is SimpleIdentifier) { + var identifier = exp; + if (!identifier.name.contains('\$')) { + _check(expression); + } + } else if (exp is ThisExpression) { + _check(expression); + } + } + } + + void _check(InterpolationExpression expression) { + var bracket = expression.rightBracket; + if (bracket != null && !isIdentifierPart(bracket.next)) { + rule.reportLint(expression); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_breaks.dart b/pkg/linter/lib/src/rules/unnecessary_breaks.dart new file mode 100644 index 00000000000..941b5f2552a --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_breaks.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't use explicit `break`s when a break is implied."; + +const _details = r''' +Only use a `break` in a non-empty switch case statement if you need to break +before the end of the case body. Dart does not support fallthrough execution +for non-empty cases, so `break`s at the end of non-empty switch case statements +are unnecessary. + +**BAD:** +```dart +switch (1) { + case 1: + print("one"); + break; + case 2: + print("two"); + break; +} +``` + +**GOOD:** +```dart +switch (1) { + case 1: + print("one"); + case 2: + print("two"); +} +``` + +```dart +switch (1) { + case 1: + case 2: + print("one or two"); +} +``` + +```dart +switch (1) { + case 1: + break; + case 2: + print("just two"); +} +``` + +NOTE: This lint only reports unnecessary breaks in libraries with a +[language version](https://dart.dev/guides/language/evolution#language-versioning) +of 3.0 or greater. Explicit breaks are still required in Dart 2.19 and below. +'''; + +class UnnecessaryBreaks extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_breaks', "Unnecessary 'break' statement.", + correctionMessage: "Try removing the 'break'."); + + UnnecessaryBreaks() + : super( + name: 'unnecessary_breaks', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.patterns)) return; + var visitor = _Visitor(this); + registry.addBreakStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + visitBreakStatement(BreakStatement node) { + if (node.label != null) return; + var parent = node.parent; + if (parent is SwitchPatternCase) { + var statements = parent.statements; + if (statements.length == 1) return; + if (node == statements.last) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_const.dart b/pkg/linter/lib/src/rules/unnecessary_const.dart new file mode 100644 index 00000000000..646704fca74 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_const.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid const keyword.'; + +const _details = r''' +**AVOID** repeating const keyword in a const context. + +**BAD:** +```dart +class A { const A(); } +m(){ + const a = const A(); + final b = const [const A()]; +} +``` + +**GOOD:** +```dart +class A { const A(); } +m(){ + const a = A(); + final b = const [A()]; +} +``` + +'''; + +class UnnecessaryConst extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'unnecessary_const', "Unnecessary 'const' keyword.", + correctionMessage: 'Try removing the keyword.'); + + UnnecessaryConst() + : super( + name: 'unnecessary_const', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + registry.addListLiteral(this, visitor); + registry.addRecordLiteral(this, visitor); + registry.addSetOrMapLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.keyword?.type != Keyword.CONST) return; + + if (node.inConstantContext) { + rule.reportLint(node); + } + } + + @override + void visitListLiteral(ListLiteral node) { + if (node.unParenthesized.parent is ConstantPattern) return; + _visitTypedLiteral(node); + } + + @override + void visitRecordLiteral(RecordLiteral node) { + if (node.constKeyword == null) return; + + if (node.inConstantContext) { + rule.reportLint(node); + } + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + if (node.unParenthesized.parent is ConstantPattern) return; + _visitTypedLiteral(node); + } + + void _visitTypedLiteral(TypedLiteral node) { + if (node.constKeyword?.type != Keyword.CONST) return; + + if (node.inConstantContext) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_constructor_name.dart b/pkg/linter/lib/src/rules/unnecessary_constructor_name.dart new file mode 100644 index 00000000000..babe6d1793b --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_constructor_name.dart @@ -0,0 +1,98 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary `.new` constructor name.'; + +const _details = r''' +**PREFER** using the default unnamed Constructor over `.new`. + +Given a class `C`, the named unnamed constructor `C.new` refers to the same +constructor as the unnamed `C`. As such it adds nothing but visual noise to +invocations and should be avoided (unless being used to identify a constructor +tear-off). + +**BAD:** +```dart +class A { + A.new(); // LINT +} + +var a = A.new(); // LINT +``` + +**GOOD:** +```dart +class A { + A.ok(); +} + +var a = A(); +var aa = A.ok(); +var makeA = A.new; +``` +'''; + +class UnnecessaryConstructorName extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_constructor_name', "Unnecessary '.new' constructor name.", + correctionMessage: "Try removing the '.new'."); + + UnnecessaryConstructorName() + : super( + name: 'unnecessary_constructor_name', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addConstructorDeclaration(this, visitor); + registry.addRepresentationConstructorName(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var parent = node.parent; + if (parent is ExtensionTypeDeclaration && + parent.representation.constructorName == null) { + return; + } + + _check(node.name); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + _check(node.constructorName.name?.token); + } + + @override + void visitRepresentationConstructorName(RepresentationConstructorName node) { + _check(node.name); + } + + void _check(Token? name) { + if (name?.lexeme == 'new') { + rule.reportLintForToken(name); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_final.dart b/pkg/linter/lib/src/rules/unnecessary_final.dart new file mode 100644 index 00000000000..7992cd818bb --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_final.dart @@ -0,0 +1,151 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = "Don't use `final` for local variables."; + +const _details = r''' +Use `var`, not `final`, when declaring local variables. + +Per [Effective Dart](https://dart.dev/effective-dart/usage#do-follow-a-consistent-rule-for-var-and-final-on-local-variables), +there are two styles in wide use. This rule enforces the `var` style. +For the alternative style that prefers `final`, enable `prefer_final_locals` +and `prefer_final_in_for_each` instead. + +For fields, `final` is always recommended; see the rule `prefer_final_fields`. + +**BAD:** +```dart +void badMethod() { + final label = 'Final or var?'; + for (final char in ['v', 'a', 'r']) { + print(char); + } +} +``` + +**GOOD:** +```dart +void goodMethod() { + var label = 'Final or var?'; + for (var char in ['v', 'a', 'r']) { + print(char); + } +} +``` +'''; + +class UnnecessaryFinal extends LintRule { + static const LintCode withType = LintCode( + 'unnecessary_final', "Local variables should not be marked as 'final'.", + correctionMessage: "Remove the 'final'."); + + static const LintCode withoutType = LintCode( + 'unnecessary_final', "Local variables should not be marked as 'final'.", + correctionMessage: "Replace 'final' with 'var'."); + + UnnecessaryFinal() + : super( + name: 'unnecessary_final', + description: _desc, + details: _details, + group: Group.style); + + @override + List get incompatibleRules => + const ['prefer_final_locals', 'prefer_final_parameters']; + + @override + List get lintCodes => [withType, withoutType]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry + ..addFormalParameterList(this, visitor) + ..addForStatement(this, visitor) + ..addDeclaredVariablePattern(this, visitor) + ..addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + LintCode getErrorCode(Object? type) => + type == null ? UnnecessaryFinal.withoutType : UnnecessaryFinal.withType; + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + var keyword = node.keyword; + keyword ??= + node.thisOrAncestorOfType()?.keyword; + if (keyword?.type != Keyword.FINAL) return; + + var errorCode = getErrorCode(node.matchedValueType); + rule.reportLintForToken(keyword, errorCode: errorCode); + } + + @override + void visitFormalParameterList(FormalParameterList parameterList) { + for (var node in parameterList.parameters) { + if (node.isFinal) { + var (keyword, type) = getParameterDetails(node); + if (keyword == null) continue; + + var errorCode = getErrorCode(type); + rule.reportLintForToken(keyword, errorCode: errorCode); + } + } + } + + @override + void visitForStatement(ForStatement node) { + var forLoopParts = node.forLoopParts; + // If the following `if` test fails, then either the statement is not a + // for-each loop, or it is something like `for(a in b) { ... }`. In the + // second case, notice `a` is not actually declared from within the + // loop. `a` is a variable declared outside the loop. + if (forLoopParts is ForEachPartsWithDeclaration) { + var loopVariable = forLoopParts.loopVariable; + if (loopVariable.isFinal) { + var errorCode = getErrorCode(loopVariable.type); + rule.reportLintForToken(loopVariable.keyword, errorCode: errorCode); + } + } else if (forLoopParts is ForEachPartsWithPattern) { + var keyword = forLoopParts.keyword; + if (keyword.isFinal) { + rule.reportLintForToken(keyword, + errorCode: UnnecessaryFinal.withoutType); + } + } + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + if (node.variables.isFinal) { + var errorCode = getErrorCode(node.variables.type); + rule.reportLintForToken(node.variables.keyword, errorCode: errorCode); + } + } + + (Token?, AstNode?) getParameterDetails(FormalParameter node) { + var parameter = node is DefaultFormalParameter ? node.parameter : node; + return switch (parameter) { + FieldFormalParameter() => (parameter.keyword, parameter.type), + SimpleFormalParameter() => (parameter.keyword, parameter.type), + SuperFormalParameter() => (parameter.keyword, parameter.type), + _ => (null, null), + }; + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_getters_setters.dart b/pkg/linter/lib/src/rules/unnecessary_getters_setters.dart new file mode 100644 index 00000000000..5aa42172712 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_getters_setters.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = + r'Avoid wrapping fields in getters and setters just to be "safe".'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#dont-wrap-a-field-in-a-getter-and-setter-unnecessarily): + +**AVOID** wrapping fields in getters and setters just to be "safe". + +In Java and C#, it's common to hide all fields behind getters and setters (or +properties in C#), even if the implementation just forwards to the field. That +way, if you ever need to do more work in those members, you can do it without needing +to touch the callsites. This is because calling a getter method is different +than accessing a field in Java, and accessing a property isn't binary-compatible +with accessing a raw field in C#. + +Dart doesn't have this limitation. Fields and getters/setters are completely +indistinguishable. You can expose a field in a class and later wrap it in a +getter and setter without having to touch any code that uses that field. + +**BAD:** +```dart +class Box { + var _contents; + get contents => _contents; + set contents(value) { + _contents = value; + } +} +``` + +**GOOD:** +```dart +class Box { + var contents; +} +``` + +'''; + +class UnnecessaryGettersSetters extends LintRule { + static const LintCode code = LintCode('unnecessary_getters_setters', + 'Unnecessary use of getter and setter to wrap a field.', + correctionMessage: + 'Try removing the getter and setter and renaming the field.'); + + UnnecessaryGettersSetters() + : super( + name: 'unnecessary_getters_setters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + _check(node.members); + } + + void _check(NodeList members) { + var getters = {}; + var setters = {}; + + // Build getter/setter maps + for (var method in members.whereType()) { + var methodName = method.name.lexeme; + if (method.isGetter) { + getters[methodName] = method; + } else if (method.isSetter) { + setters[methodName] = method; + } + } + + // Only select getters with setter pairs + for (var id in getters.keys) { + _visitGetterSetter(getters[id]!, setters[id]); + } + } + + void _visitGetterSetter(MethodDeclaration getter, MethodDeclaration? setter) { + if (setter == null) return; + var getterElement = getter.declaredElement; + var setterElement = setter.declaredElement; + if (getterElement == null || setterElement == null) return; + if (isSimpleSetter(setter) && + isSimpleGetter(getter) && + getterElement.metadata.isEmpty && + setterElement.metadata.isEmpty) { + // Just flag the getter (https://github.com/dart-lang/linter/issues/2851) + rule.reportLintForToken(getter.name); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_lambdas.dart b/pkg/linter/lib/src/rules/unnecessary_lambdas.dart new file mode 100644 index 00000000000..31fea93f3aa --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_lambdas.dart @@ -0,0 +1,262 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/dart_type_utilities.dart'; + +const _desc = r"Don't create a lambda when a tear-off will do."; + +const _details = r''' +**DON'T** create a lambda when a tear-off will do. + +**BAD:** +```dart +names.forEach((name) { + print(name); +}); +``` + +**GOOD:** +```dart +names.forEach(print); +``` + +'''; + +Iterable _extractElementsOfSimpleIdentifiers(AstNode node) => + _IdentifierVisitor().extractElements(node); + +class UnnecessaryLambdas extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_lambdas', 'Closure should be a tearoff.', + correctionMessage: 'Try using a tearoff rather than a closure.'); + + UnnecessaryLambdas() + : super( + name: 'unnecessary_lambdas', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addFunctionExpression(this, visitor); + } +} + +class _FinalExpressionChecker { + final Set parameters; + + _FinalExpressionChecker(this.parameters); + + /// Returns whether [element] is a `final` variable or property and not + /// `late`. + bool isFinalElement(Element? element) { + if (element is PropertyAccessorElement) { + return element.isSynthetic && + element.variable.isFinal && + !element.variable.isLate; + } else if (element is VariableElement) { + return element.isFinal && !element.isLate; + } + return true; + } + + bool isFinalNode(Expression? node) { + if (node == null) { + return true; + } + + if (node is FunctionExpression) { + var referencedElements = _extractElementsOfSimpleIdentifiers(node); + return !referencedElements.any(parameters.contains); + } + + if (node is ParenthesizedExpression) { + return isFinalNode(node.expression); + } + + if (node is PrefixedIdentifier) { + return isFinalNode(node.prefix) && isFinalNode(node.identifier); + } + + if (node is PropertyAccess) { + return isFinalNode(node.target) && isFinalNode(node.propertyName); + } + + if (node is SimpleIdentifier) { + var element = node.staticElement; + if (parameters.contains(element)) { + return false; + } + return isFinalElement(element); + } + + return false; + } +} + +class _IdentifierVisitor extends RecursiveAstVisitor { + final _elements = []; + + _IdentifierVisitor(); + + List extractElements(AstNode node) { + node.accept(this); + return _elements; + } + + @override + visitSimpleIdentifier(SimpleIdentifier node) { + _elements.add(node.staticElement); + super.visitSimpleIdentifier(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final bool constructorTearOffsEnabled; + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context) + : constructorTearOffsEnabled = + context.isEnabled(Feature.constructor_tearoffs); + + @override + void visitFunctionExpression(FunctionExpression node) { + if (node.declaredElement?.name != '' || node.body.keyword != null) { + return; + } + var body = node.body; + if (body is BlockFunctionBody && body.block.statements.length == 1) { + var statement = body.block.statements.single; + if (statement is ExpressionStatement && + statement.expression is InvocationExpression) { + _visitInvocationExpression( + statement.expression as InvocationExpression, node); + } else if (statement is ReturnStatement && + statement.expression is InvocationExpression) { + var expression = statement.expression; + if (expression is InvocationExpression) { + _visitInvocationExpression(expression, node); + } + } + } else if (body is ExpressionFunctionBody) { + var expression = body.expression; + if (expression is InvocationExpression) { + _visitInvocationExpression(expression, node); + } else if (constructorTearOffsEnabled && + expression is InstanceCreationExpression) { + _visitInstanceCreation(expression, node); + } + } + } + + void _visitInstanceCreation( + InstanceCreationExpression expression, FunctionExpression node) { + if (expression.isConst) return; + + var arguments = expression.argumentList.arguments; + var parameters = node.parameters?.parameters ?? []; + if (parameters.length != arguments.length) return; + + bool matches(Expression argument, FormalParameter parameter) { + if (argument is SimpleIdentifier) { + return argument.name == parameter.name?.lexeme; + } + return false; + } + + for (var i = 0; i < arguments.length; ++i) { + if (!matches(arguments[i], parameters[i])) { + return; + } + } + + rule.reportLint(node); + } + + void _visitInvocationExpression( + InvocationExpression node, FunctionExpression nodeToLint) { + var nodeToLintParams = nodeToLint.parameters?.parameters; + if (nodeToLintParams == null || + !argumentsMatchParameters( + node.argumentList.arguments, nodeToLintParams)) { + return; + } + + bool isTearoffAssignable(DartType? assignedType) { + if (assignedType != null) { + var tearoffType = node.staticInvokeType; + if (tearoffType == null || + !context.typeSystem.isSubtypeOf(tearoffType, assignedType)) { + return false; + } + } + return true; + } + + var parameters = nodeToLintParams.map((e) => e.declaredElement).toSet(); + if (node is FunctionExpressionInvocation) { + // todo (pq): consider checking for assignability + // see: https://github.com/dart-lang/linter/issues/1561 + var checker = _FinalExpressionChecker(parameters); + if (checker.isFinalNode(node.function)) { + rule.reportLint(nodeToLint); + } + } else if (node is MethodInvocation) { + var target = node.target; + if (target is SimpleIdentifier) { + var element = target.staticElement; + if (element is PrefixElement) { + for (var import in element.imports) { + if (import.isDeferred) { + return; + } + } + } + } + + var parent = nodeToLint.parent; + if (parent is NamedExpression) { + var argType = parent.staticType; + if (!isTearoffAssignable(argType)) { + return; + } + } else if (parent is VariableDeclaration) { + var grandparent = parent.parent; + if (grandparent is VariableDeclarationList) { + var variableType = grandparent.type?.type; + if (!isTearoffAssignable(variableType)) { + return; + } + } + } + + var checker = _FinalExpressionChecker(parameters); + if (!node.containsNullAwareInvocationInChain() && + checker.isFinalNode(node.target) && + checker.isFinalElement(node.methodName.staticElement) && + node.typeArguments == null) { + rule.reportLint(nodeToLint); + } + } + } +} + +extension LibraryImportElementExtension on LibraryImportElement { + bool get isDeferred => prefix is DeferredImportElementPrefix; +} diff --git a/pkg/linter/lib/src/rules/unnecessary_late.dart b/pkg/linter/lib/src/rules/unnecessary_late.dart new file mode 100644 index 00000000000..712238ac45c --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_late.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't specify the `late` modifier when it is not needed."; + +const _details = r''' +**DO** not specify the `late` modifier for top-level and static variables +when the declaration contains an initializer. + +Top-level and static variables with initializers are already evaluated lazily +as if they are marked `late`. + +**BAD:** +```dart +late String badTopLevel = ''; +``` + +**GOOD:** +```dart +String goodTopLevel = ''; +``` + +**BAD:** +```dart +class BadExample { + static late String badStatic = ''; +} +``` + +**GOOD:** +```dart +class GoodExample { + late String goodStatic; +} +``` +'''; + +class UnnecessaryLate extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_late', "Unnecessary 'late' modifier.", + correctionMessage: "Try removing the 'late'."); + + UnnecessaryLate() + : super( + name: 'unnecessary_late', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFieldDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (node.isStatic) { + _visitVariableDeclarations(node.fields); + } + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + _visitVariableDeclarations(node.variables); + } + + void _visitVariableDeclarations(VariableDeclarationList node) { + if (node.lateKeyword == null) { + return; + } + + for (var variable in node.variables) { + if (variable.initializer != null) { + rule.reportLintForToken(variable.name); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_library_directive.dart b/pkg/linter/lib/src/rules/unnecessary_library_directive.dart new file mode 100644 index 00000000000..34eed3c57be --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_library_directive.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _name = 'unnecessary_library_directive'; + +const _desc = + 'Avoid library directives unless they have documentation comments or ' + 'annotations.'; + +const _details = r''' +**DO** use library directives if you want to document a library and/or annotate +a library. + +**BAD:** +```dart +library; +``` + +**GOOD:** +```dart +/// This library does important things +library; +``` + +```dart +@TestOn('js') +library; +``` + +NOTE: Due to limitations with this lint, libraries with parts will not be +flagged for unnecessary library directives. +'''; + +class UnnecessaryLibraryDirective extends LintRule { + static const LintCode code = LintCode( + _name, + 'Library directives without comments or annotations should be avoided.', + correctionMessage: 'Try deleting the library directive.', + ); + + UnnecessaryLibraryDirective() + : super( + name: _name, + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this); + registry.addLibraryDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitLibraryDirective(LibraryDirective node) { + var parent = node.parent! as CompilationUnit; + if (parent.directives.any((element) => element is PartDirective)) { + // Parts may still use library names. No be safe, we don't lint those + // libraries – even though using library names itself is discouraged. + return; + } + + if (node.sortedCommentAndAnnotations.isEmpty) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_new.dart b/pkg/linter/lib/src/rules/unnecessary_new.dart new file mode 100644 index 00000000000..44bbf5a5152 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_new.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary new keyword.'; + +const _details = r''' +**AVOID** new keyword to create instances. + +**BAD:** +```dart +class A { A(); } +m(){ + final a = new A(); +} +``` + +**GOOD:** +```dart +class A { A(); } +m(){ + final a = A(); +} +``` + +'''; + +class UnnecessaryNew extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'unnecessary_new', "Unnecessary 'new' keyword.", + correctionMessage: "Try removing the 'new' keyword."); + + UnnecessaryNew() + : super( + name: 'unnecessary_new', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.keyword?.type == Keyword.NEW) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_null_aware_assignments.dart b/pkg/linter/lib/src/rules/unnecessary_null_aware_assignments.dart new file mode 100644 index 00000000000..6ecfe6f8c58 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_null_aware_assignments.dart @@ -0,0 +1,73 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid null in null-aware assignment.'; + +const _details = r''' +**AVOID** `null` in null-aware assignment. + +Using `null` on the right-hand side of a null-aware assignment effectively makes +the assignment redundant. + +**BAD:** +```dart +var x; +x ??= null; +``` + +**GOOD:** +```dart +var x; +x ??= 1; +``` + +'''; + +class UnnecessaryNullAwareAssignments extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_null_aware_assignments', "Unnecessary assignment of 'null'.", + correctionMessage: 'Try removing the assignment.'); + + UnnecessaryNullAwareAssignments() + : super( + name: 'unnecessary_null_aware_assignments', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAssignmentExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.readElement is PropertyAccessorElement) return; + if (node.writeElement is PropertyAccessorElement) return; + + if (node.operator.type == TokenType.QUESTION_QUESTION_EQ && + node.rightHandSide.isNullLiteral) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_null_aware_operator_on_extension_on_nullable.dart b/pkg/linter/lib/src/rules/unnecessary_null_aware_operator_on_extension_on_nullable.dart new file mode 100644 index 00000000000..a81f2e54ca6 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_null_aware_operator_on_extension_on_nullable.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Unnecessary null aware operator on extension on a nullable type.'; + +const _details = r''' +Avoid null aware operators for members defined in an extension on a nullable type. + +**BAD:** + +```dart +extension E on int? { + int m() => 1; +} +f(int? i) => i?.m(); +``` + +**GOOD:** + +```dart +extension E on int? { + int m() => 1; +} +f(int? i) => i.m(); +``` + +'''; + +class UnnecessaryNullAwareOperatorOnExtensionOnNullable extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_null_aware_operator_on_extension_on_nullable', + 'Unnecessary use of a null-aware operator to invoke an extension method ' + 'on a nullable type.', + correctionMessage: "Try removing the '?'."); + + UnnecessaryNullAwareOperatorOnExtensionOnNullable() + : super( + name: 'unnecessary_null_aware_operator_on_extension_on_nullable', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addIndexExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + registry.addPropertyAccess(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitIndexExpression(IndexExpression node) { + if (node.isNullAware && + _isExtensionOnNullableType(node.inSetterContext() + ? node + .thisOrAncestorOfType() + ?.writeElement + ?.enclosingElement + : node.staticElement?.enclosingElement)) { + rule.reportLintForToken(node.question); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.isNullAware && + _isExtensionOnNullableType( + node.methodName.staticElement?.enclosingElement)) { + rule.reportLintForToken(node.operator); + } + } + + @override + void visitPropertyAccess(PropertyAccess node) { + if (node.isNullAware) { + var realParent = node.thisOrAncestorMatching( + (p) => p != node && p is! ParenthesizedExpression); + if (_isExtensionOnNullableType(realParent is AssignmentExpression + ? realParent.writeElement?.enclosingElement + : node.propertyName.staticElement?.enclosingElement)) { + rule.reportLintForToken(node.operator); + } + } + } + + bool _isExtensionOnNullableType(Element? enclosingElement) => + enclosingElement is ExtensionElement && + context.typeSystem.isNullable(enclosingElement.extendedType); +} diff --git a/pkg/linter/lib/src/rules/unnecessary_null_checks.dart b/pkg/linter/lib/src/rules/unnecessary_null_checks.dart new file mode 100644 index 00000000000..cfad8db4ba1 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_null_checks.dart @@ -0,0 +1,206 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary null checks.'; + +const _details = r''' +**DON'T** apply a null check when a nullable value is accepted. + +**BAD:** +```dart +f(int? i) {} +m() { + int? j; + f(j!); +} + +``` + +**GOOD:** +```dart +f(int? i) {} +m() { + int? j; + f(j); +} +``` + +'''; + +DartType? getExpectedType(PostfixExpression node) { + var realNode = + node.thisOrAncestorMatching((e) => e.parent is! ParenthesizedExpression); + var parent = realNode?.parent; + var withAwait = parent is AwaitExpression; + if (withAwait) { + parent = parent.parent; + } + + // in return value + if (parent is ReturnStatement || parent is ExpressionFunctionBody) { + var parentExpression = parent?.thisOrAncestorOfType(); + if (parentExpression == null) { + return null; + } + var staticType = parentExpression.staticType; + if (staticType is! FunctionType) { + return null; + } + staticType = staticType.returnType; + if (withAwait || parentExpression.body.keyword?.lexeme == 'async') { + return staticType.isDartAsyncFuture || staticType.isDartAsyncFutureOr + ? (staticType as ParameterizedType?)?.typeArguments.first + : null; + } else { + return staticType; + } + } + // in yield value + if (parent is YieldStatement) { + var parentExpression = parent.thisOrAncestorOfType(); + if (parentExpression == null) { + return null; + } + var staticType = parentExpression.staticType; + if (staticType is! FunctionType) { + return null; + } + staticType = staticType.returnType; + return staticType.isDartCoreIterable || staticType.isDartAsyncStream + ? (staticType as ParameterizedType).typeArguments.first + : null; + } + // assignment + if (parent is AssignmentExpression && + parent.operator.type == TokenType.EQ && + (parent.leftHandSide is! Identifier || + node.operand is! Identifier || + (parent.leftHandSide as Identifier).name != + (node.operand as Identifier).name)) { + return parent.writeType; + } + // in variable declaration + if (parent is VariableDeclaration) { + return parent.declaredElement?.type; + } + // as right member of binary operator + if (parent is BinaryExpression && parent.rightOperand == realNode) { + var parentElement = parent.staticElement; + if (parentElement == null) { + return null; + } + return parentElement.parameters.first.type; + } + // as member of list + if (parent is ListLiteral) { + return (parent.staticType as ParameterizedType?)?.typeArguments.first; + } + // as member of set + if (parent is SetOrMapLiteral && parent.isSet) { + return (parent.staticType as ParameterizedType?)?.typeArguments.first; + } + // as member of map + if (parent is MapLiteralEntry) { + var grandParent = parent.parent; + while (true) { + if (grandParent is ForElement) { + grandParent = grandParent.parent; + } else if (grandParent is IfElement) { + grandParent = grandParent.parent; + } else if (grandParent is SetOrMapLiteral) { + var type = grandParent.staticType as InterfaceType?; + return type?.typeArguments[parent.key == node ? 0 : 1]; + } else { + return null; + } + } + } + // as parameter of function + if (parent is NamedExpression) { + realNode = parent; + parent = parent.parent; + } + if (parent is ArgumentList && realNode is Expression) { + var grandParent = parent.parent; + if (grandParent is InstanceCreationExpression) { + var constructor = grandParent.constructorName.staticElement; + if (constructor != null) { + if (constructor.returnType.isDartAsyncFuture && + constructor.name == 'value') { + return null; + } + } + } else if (grandParent is MethodInvocation) { + var targetType = grandParent.realTarget?.staticType; + if (targetType is InterfaceType) { + var targetClass = targetType.element; + + if (targetClass.library.isDartAsync && + targetClass.name == 'Completer' && + grandParent.methodName.name == 'complete') { + return null; + } + } + } + return realNode.staticParameterElement?.type; + } + return null; +} + +class UnnecessaryNullChecks extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_null_checks', "Unnecessary use of a null check ('!').", + correctionMessage: 'Try removing the null check.'); + + UnnecessaryNullChecks() + : super( + name: 'unnecessary_null_checks', + description: _desc, + details: _details, + state: State.experimental(), + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addNullAssertPattern(this, visitor); + registry.addPostfixExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitNullAssertPattern(NullAssertPattern node) { + var expectedType = node.matchedValueType; + if (expectedType != null && context.typeSystem.isNullable(expectedType)) { + rule.reportLintForToken(node.operator); + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + if (node.operator.type != TokenType.BANG) return; + + var expectedType = getExpectedType(node); + if (expectedType != null && context.typeSystem.isNullable(expectedType)) { + rule.reportLintForToken(node.operator); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_null_in_if_null_operators.dart b/pkg/linter/lib/src/rules/unnecessary_null_in_if_null_operators.dart new file mode 100644 index 00000000000..c0069c7e38a --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_null_in_if_null_operators.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Avoid using `null` in `if null` operators.'; + +const _details = r''' +**AVOID** using `null` as an operand in `if null` operators. + +Using `null` in an `if null` operator is redundant, regardless of which side +`null` is used on. + +**BAD:** +```dart +var x = a ?? null; +var y = null ?? 1; +``` + +**GOOD:** +```dart +var x = a ?? 1; +``` + +'''; + +class UnnecessaryNullInIfNullOperators extends LintRule { + static const LintCode code = LintCode('unnecessary_null_in_if_null_operators', + "Unnecessary use of '??' with 'null'.", + correctionMessage: + "Try removing the '??' operator and the 'null' operand."); + + UnnecessaryNullInIfNullOperators() + : super( + name: 'unnecessary_null_in_if_null_operators', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitBinaryExpression(BinaryExpression node) { + if (node.operator.type == TokenType.QUESTION_QUESTION && + (node.rightOperand.isNullLiteral || node.leftOperand.isNullLiteral)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_nullable_for_final_variable_declarations.dart b/pkg/linter/lib/src/rules/unnecessary_nullable_for_final_variable_declarations.dart new file mode 100644 index 00000000000..82fa6ba8c44 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_nullable_for_final_variable_declarations.dart @@ -0,0 +1,137 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use a non-nullable type for a final variable initialized ' + 'with a non-nullable value.'; + +const _details = r''' +Use a non-nullable type for a final variable initialized with a non-nullable +value. + +**BAD:** +```dart +final int? i = 1; +``` + +**GOOD:** +```dart +final int i = 1; +``` + +'''; + +class UnnecessaryNullableForFinalVariableDeclarations extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_nullable_for_final_variable_declarations', + 'Type could be non-nullable.', + correctionMessage: 'Try changing the type to be non-nullable.'); + + UnnecessaryNullableForFinalVariableDeclarations() + : super( + name: 'unnecessary_nullable_for_final_variable_declarations', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) { + return; + } + + var visitor = _Visitor(this, context); + registry.addFieldDeclaration(this, visitor); + registry.addPatternVariableDeclaration(this, visitor); + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addVariableDeclarationStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + void check(AstNode element) { + if (element is! DeclaredVariablePattern) return; + var type = element.declaredElement?.type; + if (type == null) return; + if (type is DynamicType) return; + var valueType = element.matchedValueType; + if (valueType == null) return; + if (context.typeSystem.isNullable(type) && + context.typeSystem.isNonNullable(valueType)) { + rule.reportLint(element); + } + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + for (var variable in node.fields.variables) { + if (Identifier.isPrivateName(variable.name.lexeme) || node.isStatic) { + _visit(variable); + } + } + } + + @override + void visitPatternVariableDeclaration(PatternVariableDeclaration node) { + if (node.keyword.keyword != Keyword.FINAL) return; + + var pattern = node.pattern; + if (pattern is RecordPattern) { + for (var field in pattern.fields) { + check(field.pattern); + } + } + if (pattern is ListPattern) { + pattern.elements.forEach(check); + } + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + node.variables.variables.forEach(_visit); + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + node.variables.variables.forEach(_visit); + } + + void _visit(VariableDeclaration variable) { + if (!variable.isFinal && !variable.isConst) { + return; + } + if (variable.isSynthetic) { + return; + } + var initializerType = variable.initializer?.staticType; + if (initializerType == null) { + return; + } + var declaredElement = variable.declaredElement; + if (declaredElement == null || declaredElement.type is DynamicType) { + return; + } + + if (context.typeSystem.isNullable(declaredElement.type) && + context.typeSystem.isNonNullable(initializerType)) { + rule.reportLint(variable); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_overrides.dart b/pkg/linter/lib/src/rules/unnecessary_overrides.dart new file mode 100644 index 00000000000..3ddab71a7ab --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_overrides.dart @@ -0,0 +1,381 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/dart_type_utilities.dart'; + +const _desc = + r"Don't override a method to do a super method invocation with the same" + r' parameters.'; + +const _details = r''' +**DON'T** override a method to do a super method invocation with same parameters. + +**BAD:** +```dart +class A extends B { + @override + void foo() { + super.foo(); + } +} +``` + +**GOOD:** +```dart +class A extends B { + @override + void foo() { + doSomethingElse(); + } +} +``` + +It's valid to override a member in the following cases: + +* if a type (return type or a parameter type) is not the exactly the same as the + super member, +* if the `covariant` keyword is added to one of the parameters, +* if documentation comments are present on the member, +* if the member has annotations other than `@override`, +* if the member is not annotated with `@protected`, and the super member is. + +`noSuchMethod` is a special method and is not checked by this rule. + +'''; + +class UnnecessaryOverrides extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_overrides', 'Unnecessary override.', + correctionMessage: + 'Try adding behavior in the overriding member or removing the override.'); + + UnnecessaryOverrides() + : super( + name: 'unnecessary_overrides', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +abstract class _AbstractUnnecessaryOverrideVisitor extends SimpleAstVisitor { + final LintRule rule; + + /// If [declaration] is an inherited member of interest, then this is set in + /// [visitMethodDeclaration]. + late ExecutableElement _inheritedMethod; + late MethodDeclaration declaration; + + _AbstractUnnecessaryOverrideVisitor(this.rule); + + ExecutableElement? getInheritedElement(MethodDeclaration node); + + @override + void visitBlock(Block node) { + if (node.statements.length == 1) { + node.statements.first.accept(this); + } + } + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + visitBlock(node.block); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + node.expression.accept(this); + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + node.expression.accept(this); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + // 'noSuchMethod' is mandatory to proxify. + if (node.name.lexeme == 'noSuchMethod') return; + + // It's ok to override to have better documentation. + if (node.documentationComment != null) return; + + var inheritedMethod = getInheritedElement(node); + if (inheritedMethod == null) return; + _inheritedMethod = inheritedMethod; + declaration = node; + + // It's ok to override to add annotations. + if (_addsMetadata()) return; + + // It's ok to override to change the signature. + if (!_haveSameDeclaration()) return; + + // It's ok to override to make a `@protected` method public. + if (_makesPublicFromProtected()) return; + + node.body.accept(this); + } + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + node.unParenthesized.accept(this); + } + + @override + void visitReturnStatement(ReturnStatement node) { + if (node.beginToken.precedingComments != null) return; + node.expression?.accept(this); + } + + @override + void visitSuperExpression(SuperExpression node) { + if (node.beginToken.precedingComments != null) return; + rule.reportLintForToken(declaration.name); + } + + /// Returns whether [declaration] is annotated with any metadata (other than + /// `@override` or `@Override`). + bool _addsMetadata() { + var metadata = declaration.declaredElement?.metadata; + if (metadata != null) { + for (var annotation in metadata) { + if (annotation.isOverride) continue; + if (annotation.isProtected && _inheritedMethod.hasProtected) continue; + + // Any other annotation implies a meaningful override. + return true; + } + } + return false; + } + + bool _haveSameDeclaration() { + var declaredElement = declaration.declaredElement; + if (declaredElement == null) { + return false; + } + if (declaredElement.returnType != _inheritedMethod.returnType) { + return false; + } + if (declaredElement.parameters.length != + _inheritedMethod.parameters.length) { + return false; + } + for (var i = 0; i < _inheritedMethod.parameters.length; i++) { + var superParam = _inheritedMethod.parameters[i]; + var param = declaredElement.parameters[i]; + if (param.type != superParam.type) return false; + if (param.name != superParam.name) return false; + if (param.isCovariant != superParam.isCovariant) return false; + if (!_sameKind(param, superParam)) return false; + if (param.defaultValueCode != superParam.defaultValueCode) return false; + } + return true; + } + + /// Returns true if [_inheritedMethod] is `@protected` and [declaration] is + /// not `@protected`, and false otherwise. + /// + /// This indicates that [_inheritedMethod] may have been overridden in order + /// to expand its visibility. + bool _makesPublicFromProtected() { + var declaredElement = declaration.declaredElement; + if (declaredElement == null) return false; + if (declaredElement.hasProtected) { + return false; + } + return _inheritedMethod.hasProtected; + } + + bool _sameKind(ParameterElement first, ParameterElement second) { + if (first.isRequired) { + return second.isRequired; + } else if (first.isOptionalPositional) { + return second.isOptionalPositional; + } else if (first.isNamed) { + return second.isNamed; + } + throw ArgumentError('Unhandled kind of parameter.'); + } +} + +class _UnnecessaryGetterOverrideVisitor + extends _AbstractUnnecessaryOverrideVisitor { + _UnnecessaryGetterOverrideVisitor(super.rule); + + @override + ExecutableElement? getInheritedElement(MethodDeclaration node) { + var element = node.declaredElement; + if (element == null) return null; + var enclosingElement = element.enclosingElement; + if (enclosingElement is! InterfaceElement) return null; + return enclosingElement.thisType.lookUpGetter2( + element.name, + element.library, + concrete: true, + inherited: true, + ); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + if (node.propertyName.name == _inheritedMethod.name) { + node.target?.accept(this); + } + } +} + +class _UnnecessaryMethodOverrideVisitor + extends _AbstractUnnecessaryOverrideVisitor { + _UnnecessaryMethodOverrideVisitor(super.rule); + + @override + ExecutableElement? getInheritedElement(node) { + var element = node.declaredElement; + if (element == null) return null; + var enclosingElement = element.enclosingElement; + if (enclosingElement is! InterfaceElement) return null; + return enclosingElement.thisType.lookUpMethod2( + node.name.lexeme, + element.library, + concrete: true, + inherited: true, + ); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var declarationParameters = declaration.parameters; + if (declarationParameters != null && + node.methodName.name == _inheritedMethod.name && + argumentsMatchParameters( + node.argumentList.arguments, declarationParameters.parameters)) { + node.target?.accept(this); + } + } +} + +class _UnnecessaryOperatorOverrideVisitor + extends _AbstractUnnecessaryOverrideVisitor { + _UnnecessaryOperatorOverrideVisitor(super.rule); + + @override + ExecutableElement? getInheritedElement(node) { + var element = node.declaredElement; + if (element == null) return null; + var enclosingElement = element.enclosingElement; + if (enclosingElement is! InterfaceElement) return null; + return enclosingElement.thisType.lookUpMethod2( + element.name, + element.library, + concrete: true, + inherited: true, + ); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + var parameters = declaration.parameters?.parameters; + if (node.operator.type == declaration.name.type && + parameters != null && + parameters.length == 1 && + parameters.first.declaredElement == + node.rightOperand.canonicalElement) { + var leftPart = node.leftOperand.unParenthesized; + if (leftPart is SuperExpression) { + visitSuperExpression(leftPart); + } + } + } + + @override + void visitPrefixExpression(PrefixExpression node) { + var parameters = declaration.parameters?.parameters; + if (parameters != null && + node.operator.type == declaration.name.type && + parameters.isEmpty) { + var operand = node.operand.unParenthesized; + if (operand is SuperExpression) { + visitSuperExpression(operand); + } + } + } +} + +class _UnnecessarySetterOverrideVisitor + extends _AbstractUnnecessaryOverrideVisitor { + _UnnecessarySetterOverrideVisitor(super.rule); + + @override + ExecutableElement? getInheritedElement(node) { + var element = node.declaredElement; + if (element == null) return null; + var enclosingElement = element.enclosingElement; + if (enclosingElement is! InterfaceElement) return null; + return enclosingElement.thisType.lookUpSetter2( + node.name.lexeme, + element.library, + concrete: true, + inherited: true, + ); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + var parameters = declaration.parameters?.parameters; + if (parameters != null && + parameters.length == 1 && + parameters.first.declaredElement == + node.rightHandSide.canonicalElement) { + var leftPart = node.leftHandSide.unParenthesized; + if (leftPart is PropertyAccess) { + if (node.writeElement?.name == _inheritedMethod.name) { + leftPart.target?.accept(this); + } + } + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isStatic) { + return; + } + if (node.operatorKeyword != null) { + var visitor = _UnnecessaryOperatorOverrideVisitor(rule); + visitor.visitMethodDeclaration(node); + } else if (node.isGetter) { + var visitor = _UnnecessaryGetterOverrideVisitor(rule); + visitor.visitMethodDeclaration(node); + } else if (node.isSetter) { + var visitor = _UnnecessarySetterOverrideVisitor(rule); + visitor.visitMethodDeclaration(node); + } else { + var visitor = _UnnecessaryMethodOverrideVisitor(rule); + visitor.visitMethodDeclaration(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart b/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart new file mode 100644 index 00000000000..19bb702ff1b --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_parenthesis.dart @@ -0,0 +1,286 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Unnecessary parentheses can be removed.'; + +const _details = r''' +**AVOID** using parentheses when not needed. + +**BAD:** +```dart +a = (b); +``` + +**GOOD:** +```dart +a = b; +``` + +Parentheses are considered unnecessary if they do not change the meaning of the +code and they do not improve the readability of the code. The goal is not to +force all developers to maintain the expression precedence table in their heads, +which is why the second condition is included. Examples of this condition +include: + +* cascade expressions - it is sometimes not clear what the target of a cascade + expression is, especially with assignments, or nested cascades. For example, + the expression `a.b = (c..d)`. +* expressions with whitespace between tokens - it can look very strange to see + an expression like `!await foo` which is valid and equivalent to + `!(await foo)`. +* logical expressions - parentheses can improve the readability of the implicit + grouping defined by precedence. For example, the expression + `(a && b) || c && d`. +'''; + +class UnnecessaryParenthesis extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_parenthesis', 'Unnecessary use of parentheses.', + correctionMessage: 'Try removing the parentheses.'); + + UnnecessaryParenthesis() + : super( + name: 'unnecessary_parenthesis', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addParenthesizedExpression(this, visitor); + } +} + +class _ContainsFunctionExpressionVisitor extends UnifyingAstVisitor { + bool hasFunctionExpression = false; + + @override + void visitFunctionExpression(FunctionExpression node) { + hasFunctionExpression = true; + } + + @override + void visitNode(AstNode node) { + if (!hasFunctionExpression) { + node.visitChildren(this); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + var parent = node.parent; + // case const (a + b): + if (parent is ConstantPattern) return; + var expression = node.expression; + if (expression is SimpleIdentifier || + expression.containsNullAwareInvocationInChain()) { + if (parent is PropertyAccess) { + var name = parent.propertyName.name; + if (name == 'hashCode' || name == 'runtimeType') { + // Code like `(String).hashCode` is allowed. + return; + } + } else if (parent is MethodInvocation) { + var name = parent.methodName.name; + if (name == 'noSuchMethod' || name == 'toString') { + // Code like `(String).noSuchMethod()` is allowed. + return; + } + } else if (parent is PostfixExpression && + parent.operator.type == TokenType.BANG) { + return; + } + rule.reportLint(node); + return; + } + + // https://github.com/dart-lang/linter/issues/2944 + if (expression is FunctionExpression) { + if (parent is MethodInvocation || + parent is PropertyAccess || + parent is BinaryExpression || + parent is IndexExpression) { + return; + } + } + + if (expression is ConstructorReference) { + if (parent is! FunctionExpressionInvocation || + parent.typeArguments == null) { + rule.reportLint(node); + return; + } + } + + // Directly wrapped into parentheses already - always report. + if (parent is ParenthesizedExpression || + parent is InterpolationExpression || + (parent is ArgumentList && parent.arguments.length == 1) || + (parent is IfStatement && node == parent.expression) || + (parent is IfElement && node == parent.expression) || + (parent is WhileStatement && node == parent.condition) || + (parent is DoStatement && node == parent.condition) || + (parent is SwitchStatement && node == parent.expression) || + (parent is SwitchExpression && node == parent.expression)) { + rule.reportLint(node); + return; + } + + // `(foo ? bar : baz)` is OK. + if (expression is ConditionalExpression) { + return; + } + + // `a..b = (c..d)` is OK. + if (expression is CascadeExpression || + node.thisOrAncestorMatching( + (n) => n is Statement || n is CascadeExpression) + is CascadeExpression) { + return; + } + + // Constructor field initializers are rather unguarded by delimiting + // tokens, which can get confused with a function expression. See test + // cases for issues #1395 and #1473. + if (parent is ConstructorFieldInitializer && + _containsFunctionExpression(node)) { + return; + } + + // `foo = (a == b)` is OK, `return (count != 0)` is OK. + if (expression is BinaryExpression && + (expression.operator.type == TokenType.EQ_EQ || + expression.operator.type == TokenType.BANG_EQ)) { + if (parent is AssignmentExpression || + parent is VariableDeclaration || + parent is ReturnStatement || + parent is YieldStatement || + parent is ConstructorFieldInitializer) { + return; + } + } + + if (parent is Expression) { + if (parent is BinaryExpression) return; + if (parent is ConditionalExpression) return; + if (parent is CascadeExpression) return; + if (parent is FunctionExpressionInvocation) { + if (expression is PrefixedIdentifier) { + rule.reportLint(node); + } + return; + } + if (parent is AsExpression) return; + if (parent is IsExpression) return; + + // A prefix expression (! or -) can have an argument wrapped in + // "unnecessary" parens if that argument has potentially confusing + // whitespace after its first token. + if (parent is PrefixExpression && node.expression.startsWithWhitespace) { + return; + } + + // Another case of the above exception, something like + // `!(const [7]).contains(5);`, where the _parent's_ parent is the + // PrefixExpression. + if (parent is MethodInvocation) { + var target = parent.target; + if (parent.parent is PrefixExpression && + target == node && + node.expression.startsWithWhitespace) return; + } + + // Something like `({1, 2, 3}).forEach(print);`. + // The parens cannot be removed because then the curly brackets are not + // interpreted as a set-or-map literal. + if (node.wouldBeParsedAsStatementBlock) { + return; + } + + // TODO an API to the AST for better usage + // Precedence isn't sufficient (e.g. PostfixExpression requires parenthesis) + if (expression is PropertyAccess || + expression is ConstructorReference || + expression is PrefixedIdentifier || + expression is MethodInvocation || + expression is IndexExpression || + expression is Literal || + parent.precedence < expression.precedence) { + rule.reportLint(node); + } + } else { + rule.reportLint(node); + } + } + + bool _containsFunctionExpression(ParenthesizedExpression node) { + var containsFunctionExpressionVisitor = + _ContainsFunctionExpressionVisitor(); + node.accept(containsFunctionExpressionVisitor); + return containsFunctionExpressionVisitor.hasFunctionExpression; + } +} + +extension on ParenthesizedExpression { + /// Returns whether a parser would attempt to parse `this` as a statement + /// block if the parentheses were removed. + /// + /// The two components that make this true are: + /// * the parenthesized expression is a [SetOrMapLiteral] (starting with `{`), + /// * the open parenthesis of this expression is the first token of an + /// [ExpressionStatement]. + bool get wouldBeParsedAsStatementBlock { + if (expression is! SetOrMapLiteral) { + return false; + } + var exprStatementAncestor = thisOrAncestorOfType(); + if (exprStatementAncestor == null) { + return false; + } + return exprStatementAncestor.beginToken == leftParenthesis; + } +} + +extension on Expression? { + /// Returns whether this "starts" with whitespace. + /// + /// That is, is there definitely whitespace after the first token? + bool get startsWithWhitespace { + var self = this; + return + // As in, `!(await foo)`. + self is AwaitExpression || + // As in, `!(new Foo())`. + (self is InstanceCreationExpression && self.keyword != null) || + // No TypedLiteral (ListLiteral, MapLiteral, SetLiteral) accepts `-` + // or `!` as a prefix operator, but this method can be called + // recursively, so this catches things like + // `!(const [].contains(42))`. + (self is TypedLiteral && self.constKeyword != null) || + // As in, `!(const List(3).contains(7))`, and chains like + // `-(new List(3).skip(1).take(3).skip(1).length)`. + (self is MethodInvocation && self.target.startsWithWhitespace) || + // As in, `-(new List(3).length)`, and chains like + // `-(new List(3).length.bitLength.bitLength)`. + (self is PropertyAccess && self.target.startsWithWhitespace); + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_raw_strings.dart b/pkg/linter/lib/src/rules/unnecessary_raw_strings.dart new file mode 100644 index 00000000000..bc16b1817f7 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_raw_strings.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary raw string.'; + +const _details = r''' +Use raw string only when needed. + +**BAD:** +```dart +var s1 = r'a'; +``` + +**GOOD:** +```dart +var s1 = 'a'; +var s2 = r'$a'; +var s3 = r'\a'; +``` + +'''; + +class UnnecessaryRawStrings extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_raw_strings', 'Unnecessary use of a raw string.', + correctionMessage: 'Try using a normal string.'); + + UnnecessaryRawStrings() + : super( + name: 'unnecessary_raw_strings', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSimpleStringLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (node.isRaw && ![r'\', r'$'].any(node.literal.lexeme.contains)) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_statements.dart b/pkg/linter/lib/src/rules/unnecessary_statements.dart new file mode 100644 index 00000000000..4cbb8403af0 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_statements.dart @@ -0,0 +1,242 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Avoid using unnecessary statements.'; + +const _details = r''' +**AVOID** using unnecessary statements. + +Statements which have no clear effect are usually unnecessary, or should be +broken up. + +For example, + +**BAD:** +```dart +myvar; +list.clear; +1 + 2; +methodOne() + methodTwo(); +foo ? bar : baz; +``` + +Though the added methods have a clear effect, the addition itself does not +unless there is some magical overload of the + operator. + +Usually code like this indicates an incomplete thought, and is a bug. + +**GOOD:** +```dart +some.method(); +const SomeClass(); +methodOne(); +methodTwo(); +foo ? bar() : baz(); +return myvar; +``` + +'''; + +class UnnecessaryStatements extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_statements', 'Unnecessary statement.', + correctionMessage: 'Try completing the statement or breaking it up.'); + + UnnecessaryStatements() + : super( + name: 'unnecessary_statements', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(_ReportNoClearEffectVisitor(this)); + registry.addExpressionStatement(this, visitor); + registry.addForStatement(this, visitor); + registry.addCascadeExpression(this, visitor); + } +} + +class _ReportNoClearEffectVisitor extends UnifyingAstVisitor { + final LintRule rule; + + _ReportNoClearEffectVisitor(this.rule); + + @override + void visitAsExpression(AsExpression node) { + // https://github.com/dart-lang/linter/issues/2163 + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + // Has a clear effect + } + + @override + void visitAwaitExpression(AwaitExpression node) { + // Has a clear effect + } + + @override + void visitBinaryExpression(BinaryExpression node) { + switch (node.operator.lexeme) { + case '??': + case '||': + case '&&': + // these are OK when used for control flow + node.rightOperand.accept(this); + return; + } + + super.visitBinaryExpression(node); + } + + @override + void visitCascadeExpression(CascadeExpression node) { + // Has a clear effect + } + + @override + void visitConditionalExpression(ConditionalExpression node) { + node.thenExpression.accept(this); + node.elseExpression.accept(this); + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + // Has a clear effect + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + // A few APIs use this for side effects, like Timer. Also, for constructors + // that have side effects, they should have tests. Those tests will often + // include an instantiation expression statement with nothing else. + } + + @override + void visitMethodInvocation(MethodInvocation node) { + // Has a clear effect + } + + @override + void visitNode(AstNode expression) { + rule.reportLint(expression); + } + + @override + void visitPatternAssignment(PatternAssignment node) { + // Has a clear effect + } + + @override + void visitPostfixExpression(PostfixExpression node) { + // Has a clear effect + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + // Allow getters; getters with side effects were the main cause of false + // positives. + var element = node.identifier.staticElement; + if (element is PropertyAccessorElement && !element.isSynthetic) { + return; + } + + super.visitPrefixedIdentifier(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + if (node.operator.lexeme == '--' || node.operator.lexeme == '++') { + // Has a clear effect + return; + } + super.visitPrefixExpression(node); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + // Allow getters; getters with side effects were the main cause of false + // positives. + var element = node.propertyName.staticElement; + if (element is PropertyAccessorElement && !element.isSynthetic) { + return; + } + + super.visitPropertyAccess(node); + } + + @override + void visitRethrowExpression(RethrowExpression node) { + // Has a clear effect + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + // Allow getters; getters with side effects were the main cause of false + // positives. + var element = node.staticElement; + if (element is PropertyAccessorElement && !element.isSynthetic) { + return; + } + + super.visitSimpleIdentifier(node); + } + + @override + void visitSuperConstructorInvocation(SuperConstructorInvocation node) { + // Has a clear effect + } + + @override + void visitThrowExpression(ThrowExpression node) { + // Has a clear effect + } +} + +class _Visitor extends SimpleAstVisitor { + final _ReportNoClearEffectVisitor reportNoClearEffect; + + _Visitor(this.reportNoClearEffect); + @override + void visitCascadeExpression(CascadeExpression node) { + for (var section in node.cascadeSections) { + if (section is PropertyAccess && section.staticType is FunctionType) { + reportNoClearEffect.rule.reportLint(section); + } + } + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + if (node.parent is FunctionBody) { + return; + } + node.expression.accept(reportNoClearEffect); + } + + @override + void visitForStatement(ForStatement node) { + var loopParts = node.forLoopParts; + if (loopParts is ForPartsWithExpression) { + loopParts.initialization?.accept(reportNoClearEffect); + for (var u in loopParts.updaters) { + u.accept(reportNoClearEffect); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_string_escapes.dart b/pkg/linter/lib/src/rules/unnecessary_string_escapes.dart new file mode 100644 index 00000000000..dd7c85ae665 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_string_escapes.dart @@ -0,0 +1,152 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Remove unnecessary backslashes in strings.'; + +const _details = r''' +Remove unnecessary backslashes in strings. + +**BAD:** +```dart +'this string contains 2 \"double quotes\" '; +"this string contains 2 \'single quotes\' "; +``` + +**GOOD:** +```dart +'this string contains 2 "double quotes" '; +"this string contains 2 'single quotes' "; +``` + +'''; + +class UnnecessaryStringEscapes extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'unnecessary_string_escapes', 'Unnecessary escape in string literal.', + correctionMessage: "Remove the '\\' escape."); + + UnnecessaryStringEscapes() + : super( + name: 'unnecessary_string_escapes', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSimpleStringLiteral(this, visitor); + registry.addStringInterpolation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + /// The special escaped chars listed in language specification + static const allowedEscapedChars = [ + '"', + "'", + r'$', + r'\', + 'n', + 'r', + 'f', + 'b', + 't', + 'v', + 'x', + 'u', + ]; + + final LintRule rule; + + _Visitor(this.rule); + + void visitLexeme( + Token token, { + required bool isSingleQuoted, + required bool isMultiline, + required int contentsOffset, + required int contentsEnd, + }) { + // For multiline string we keep the list on pending quotes. + // Starting from 3 consecutive quotes, we allow escaping. + // index -> escaped + var pendingQuotes = {}; + void checkPendingQuotes() { + if (isMultiline && pendingQuotes.length < 3) { + var escapeIndexes = + pendingQuotes.entries.where((e) => e.value).map((e) => e.key); + for (var index in escapeIndexes) { + // case for '''___\'''' : without last backslash it leads a parsing error + if (contentsEnd != token.end && index + 2 == contentsEnd) continue; + rule.reporter.reportErrorForOffset(rule.lintCode, index, 1); + } + } + } + + var lexeme = token.lexeme + .substring(contentsOffset - token.offset, contentsEnd - token.offset); + for (var i = 0; i < lexeme.length; i++) { + var current = lexeme[i]; + var escaped = false; + if (current == r'\' && i < lexeme.length - 1) { + escaped = true; + i += 1; + current = lexeme[i]; + if (isSingleQuoted && current == '"' || + !isSingleQuoted && current == "'" || + !allowedEscapedChars.contains(current)) { + rule.reporter + .reportErrorForOffset(rule.lintCode, contentsOffset + i - 1, 1); + } + } + if (isSingleQuoted ? current == "'" : current == '"') { + pendingQuotes[contentsOffset + i - (escaped ? 1 : 0)] = escaped; + } else { + checkPendingQuotes(); + pendingQuotes.clear(); + } + } + checkPendingQuotes(); + } + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (node.isRaw) return; + + visitLexeme( + node.literal, + isSingleQuoted: node.isSingleQuoted, + isMultiline: node.isMultiline, + contentsOffset: node.contentsOffset, + contentsEnd: node.contentsEnd, + ); + } + + @override + void visitStringInterpolation(StringInterpolation node) { + for (var element in node.elements.whereType()) { + visitLexeme( + element.contents, + isSingleQuoted: node.isSingleQuoted, + isMultiline: node.isMultiline, + contentsOffset: element.contentsOffset, + contentsEnd: element.contentsEnd, + ); + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_string_interpolations.dart b/pkg/linter/lib/src/rules/unnecessary_string_interpolations.dart new file mode 100644 index 00000000000..d33a74abab6 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_string_interpolations.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; + +import '../analyzer.dart'; + +const _desc = r'Unnecessary string interpolation.'; + +const _details = r''' +**DON'T** use string interpolation if there's only a string expression in it. + +**BAD:** +```dart +String message; +String o = '$message'; +``` + +**GOOD:** +```dart +String message; +String o = message; +``` + +'''; + +class UnnecessaryStringInterpolations extends LintRule { + static const LintCode code = LintCode('unnecessary_string_interpolations', + 'Unnecessary use of string interpolation.', + correctionMessage: + 'Try replacing the string literal with the variable name.'); + + UnnecessaryStringInterpolations() + : super( + name: 'unnecessary_string_interpolations', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addStringInterpolation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitStringInterpolation(StringInterpolation node) { + if (node.parent is AdjacentStrings) return; + if (node.elements.length == 3) { + var start = node.elements.first as InterpolationString; + var interpolation = node.elements[1] as InterpolationExpression; + var end = node.elements[2] as InterpolationString; + if (start.value.isEmpty && end.value.isEmpty) { + var staticType = interpolation.expression.staticType; + if (staticType != null && + staticType.isDartCoreString && + staticType.nullabilitySuffix != NullabilitySuffix.question) { + rule.reportLint(node); + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_this.dart b/pkg/linter/lib/src/rules/unnecessary_this.dart new file mode 100644 index 00000000000..cf9552aa120 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_this.dart @@ -0,0 +1,141 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r"Don't access members with `this` unless avoiding shadowing."; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#dont-use-this-when-not-needed-to-avoid-shadowing): + +**DON'T** use `this` when not needed to avoid shadowing. + +**BAD:** +```dart +class Box { + var value; + void update(new_value) { + this.value = new_value; + } +} +``` + +**GOOD:** +```dart +class Box { + var value; + void update(new_value) { + value = new_value; + } +} +``` + +**GOOD:** +```dart +class Box { + var value; + void update(value) { + this.value = value; + } +} +``` + +'''; + +class UnnecessaryThis extends LintRule { + static const LintCode code = LintCode( + 'unnecessary_this', "Unnecessary 'this.' qualifier.", + correctionMessage: "Try removing 'this.'."); + + UnnecessaryThis() + : super( + name: 'unnecessary_this', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addConstructorFieldInitializer(this, visitor); + registry.addThisExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitConstructorFieldInitializer(ConstructorFieldInitializer node) { + if (node.thisKeyword != null) { + rule.reportLintForToken(node.thisKeyword); + } + } + + @override + void visitThisExpression(ThisExpression node) { + var parent = node.parent; + + Element? element; + if (parent is PropertyAccess && !parent.isNullAware) { + element = getWriteOrReadElement(parent.propertyName); + } else if (parent is MethodInvocation && !parent.isNullAware) { + element = parent.methodName.staticElement; + } else { + return; + } + + if (_canReferenceElementWithoutThisPrefix(element, node)) { + rule.reportLint(parent); + } + } + + bool _canReferenceElementWithoutThisPrefix(Element? element, AstNode node) { + if (element == null) { + return false; + } + + var id = element.displayName; + var isSetter = element is PropertyAccessorElement && element.isSetter; + var result = context.resolveNameInScope(id, isSetter, node); + + // No result, definitely no shadowing. + // The requested element is inherited, or from an extension. + if (result.isNone) { + return true; + } + + // The result has the matching name, might be shadowing. + // Check that the element is the same. + if (result.isRequestedName) { + return result.element == element; + } + + // The result has the same basename, but not the same name. + // Must be an instance member, so that: + // - not shadowed by a local declaration; + // - prevents us from going up to the library scope; + // - the requested element must be inherited, or from an extension. + if (result.isDifferentName) { + var enclosing = result.element?.enclosingElement; + return enclosing is ClassElement; + } + + // Should not happen. + return false; + } +} diff --git a/pkg/linter/lib/src/rules/unnecessary_to_list_in_spreads.dart b/pkg/linter/lib/src/rules/unnecessary_to_list_in_spreads.dart new file mode 100644 index 00000000000..aaac5759526 --- /dev/null +++ b/pkg/linter/lib/src/rules/unnecessary_to_list_in_spreads.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Unnecessary toList() in spreads.'; + +const _details = r''' +Unnecessary `toList()` in spreads. + +**BAD:** +```dart +children: [ + ...['foo', 'bar', 'baz'].map((String s) => Text(s)).toList(), +] +``` + +**GOOD:** +```dart +children: [ + ...['foo', 'bar', 'baz'].map((String s) => Text(s)), +] +``` + +'''; + +class UnnecessaryToListInSpreads extends LintRule { + static const LintCode code = LintCode('unnecessary_to_list_in_spreads', + "Unnecessary use of 'toList' in a spread.", + correctionMessage: "Try removing the invocation of 'toList'."); + + UnnecessaryToListInSpreads() + : super( + name: 'unnecessary_to_list_in_spreads', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSpreadElement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSpreadElement(SpreadElement node) { + var expression = node.expression; + if (expression is! MethodInvocation) { + return; + } + var target = expression.target; + if (expression.methodName.name == 'toList' && + target != null && + target.staticType.implementsInterface('Iterable', 'dart.core')) { + rule.reportLint(expression.methodName); + } + } +} diff --git a/pkg/linter/lib/src/rules/unreachable_from_main.dart b/pkg/linter/lib/src/rules/unreachable_from_main.dart new file mode 100644 index 00000000000..c852b0d453e --- /dev/null +++ b/pkg/linter/lib/src/rules/unreachable_from_main.dart @@ -0,0 +1,624 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../analyzer.dart'; + +const _desc = 'Unreachable top-level members in executable libraries.'; + +const _details = r''' +Top-level members and static members in an executable library should be used +directly inside this library. An executable library is a library that contains +a `main` top-level function or that contains a top-level function annotated with +`@pragma('vm:entry-point')`). Executable libraries are not usually imported +and it's better to avoid defining unused members. + +This rule assumes that an executable library isn't imported by other files +except to execute its `main` function. + +**BAD:** + +```dart +main() {} +void f() {} +``` + +**GOOD:** + +```dart +main() { + f(); +} +void f() {} +``` + +'''; + +class UnreachableFromMain extends LintRule { + static const LintCode code = LintCode('unreachable_from_main', + "Unreachable member '{0}' in an executable library.", + correctionMessage: 'Try referencing the member or removing it.'); + + UnreachableFromMain() + : super( + name: 'unreachable_from_main', + description: _desc, + details: _details, + group: Group.style, + state: State.stable(since: Version(3, 1, 0)), + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +/// This gathers all declarations which we may wish to report on. +class _DeclarationGatherer { + final LinterContext linterContext; + + /// All declarations which we may wish to report on. + final Set declarations = {}; + + _DeclarationGatherer({ + required this.linterContext, + }); + + void addDeclarations(CompilationUnit node) { + for (var declaration in node.declarations) { + if (declaration is TopLevelVariableDeclaration) { + declarations.addAll(declaration.variables.variables); + } else { + declarations.add(declaration); + var declaredElement = declaration.declaredElement; + if (declaredElement == null || declaredElement.isPrivate) { + continue; + } + if (declaration is ClassDeclaration) { + _addMembers( + containerElement: declaration.declaredElement, + members: declaration.members, + ); + } else if (declaration is EnumDeclaration) { + _addMembers( + containerElement: declaration.declaredElement, + members: declaration.members, + ); + } else if (declaration is ExtensionDeclaration) { + _addMembers( + containerElement: null, + members: declaration.members, + ); + } else if (declaration is MixinDeclaration) { + _addMembers( + containerElement: declaration.declaredElement, + members: declaration.members, + ); + } + } + } + } + + void _addMembers({ + required Element? containerElement, + required List members, + }) { + bool isOverride(String rawName) { + if (containerElement is! InterfaceElement) { + return false; + } + var libraryUri = containerElement.library.source.uri; + var name = Name(libraryUri, rawName); + var inheritance = linterContext.inheritanceManager; + return inheritance.getOverridden2(containerElement, name) != null; + } + + for (var member in members) { + switch (member) { + case ConstructorDeclaration(): + var e = member.declaredElement; + if (e != null && e.isPublic && member.parent is! EnumDeclaration) { + declarations.add(member); + } + case FieldDeclaration(): + for (var field in member.fields.variables) { + var element = field.declaredElement; + if (element != null && element.isPublic) { + if (!isOverride(element.name)) { + declarations.add(field); + } + } + } + case MethodDeclaration(): + var element = member.declaredElement; + if (element != null && element.isPublic) { + var rawName = member.name.lexeme; + var isTestMethod = rawName.startsWith('test_') || + rawName.startsWith('solo_test_') || + rawName == 'setUp' || + rawName == 'tearDown'; + if (!isOverride(element.name) && !isTestMethod) { + declarations.add(member); + } + } + } + } + } +} + +/// A visitor which gathers the declarations of the "references" it visits. +/// +/// "References" are most often [SimpleIdentifier]s, but can also be other +/// nodes which refer to a declaration. +class _ReferenceVisitor extends RecursiveAstVisitor { + Map declarationMap; + + Set declarations = {}; + + /// References from patterns should not be counted. + int _patternLevel = 0; + + _ReferenceVisitor(this.declarationMap); + + @override + void visitAnnotation(Annotation node) { + var e = node.element; + if (e != null) { + _addDeclaration(e); + } + super.visitAnnotation(node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _visitCompoundAssignmentExpression(node); + super.visitAssignmentExpression(node); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + _addNamedType(node.extendsClause?.superclass); + _addNamedTypes(node.withClause?.mixinTypes); + _addNamedTypes(node.implementsClause?.interfaces); + + var element = node.declaredElement; + if (element != null) { + var hasConstructors = + node.members.any((e) => e is ConstructorDeclaration); + if (!hasConstructors) { + // The default constructor will have an implicit super-initializer to + // the super-type's unnamed constructor. + _addDefaultSuperConstructorDeclaration(node); + } + + var metadata = element.metadata; + // This for-loop style is copied from analyzer's `hasX` getters on + // [Element]. + for (var i = 0; i < metadata.length; i++) { + var annotation = metadata[i].element; + if (annotation is PropertyAccessorElement && + annotation.name == 'reflectiveTest' && + annotation.library.name == 'test_reflective_loader') { + // The class is instantiated through the use of mirrors in + // 'test_reflective_loader'. + var unnamedConstructor = element.constructors + .firstWhereOrNull((constructor) => constructor.name == ''); + if (unnamedConstructor != null) { + _addDeclaration(unnamedConstructor); + } + } + } + } + super.visitClassDeclaration(node); + } + + @override + visitConstantPattern(ConstantPattern node) { + _patternLevel++; + try { + return super.visitConstantPattern(node); + } finally { + _patternLevel--; + } + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + // If a constructor does not have an explicit super-initializer (or + // redirection?) then it has an implicit super-initializer to the + // super-type's unnamed constructor. + var hasSuperInitializer = + node.initializers.any((e) => e is SuperConstructorInvocation); + if (!hasSuperInitializer) { + var enclosingClass = node.parent; + if (enclosingClass is ClassDeclaration) { + _addDefaultSuperConstructorDeclaration(enclosingClass); + } + } + super.visitConstructorDeclaration(node); + } + + @override + void visitConstructorName(ConstructorName node) { + var e = node.staticElement; + if (e != null && _patternLevel == 0) { + _addDeclaration(e); + } + super.visitConstructorName(node); + } + + @override + void visitNamedType(NamedType node) { + var element = node.element; + if (element == null) { + return; + } + + var nodeIsInTypeArgument = + node.thisOrAncestorOfType() != null; + + // Any reference to a typedef is reachable, since structural typing is used + // to match against objects. + // + // The return type of an external variable declaration is reachable, since + // the external implementation can instantiate it. + if (node.type?.alias != null || + nodeIsInTypeArgument || + node.isInExternalVariableTypeOrFunctionReturnType) { + _addDeclaration(element); + } + + // Intentionally do not add the declaration of non-alias named types, as a + // reference to such a type in a [TypeAnnotation] is not good enough to + // count as "reachable". Marking a type as reachable only because it was + // seen in a type annotation would be a miscategorization if the type is + // never instantiated or subtyped. + + var typeArguments = node.typeArguments; + if (typeArguments != null) { + for (var typeArgument in typeArguments.arguments) { + typeArgument.accept(this); + } + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + _visitCompoundAssignmentExpression(node); + super.visitPostfixExpression(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + _visitCompoundAssignmentExpression(node); + super.visitPrefixExpression(node); + } + + @override + void visitRedirectingConstructorInvocation( + RedirectingConstructorInvocation node) { + var element = node.staticElement; + if (element != null) { + _addDeclaration(element); + } + super.visitRedirectingConstructorInvocation(node); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (!node.inDeclarationContext()) { + var e = node.staticElement; + if (e != null) { + _addDeclaration(e); + } + } + super.visitSimpleIdentifier(node); + } + + @override + void visitSuperConstructorInvocation(SuperConstructorInvocation node) { + var e = node.staticElement; + if (e != null) { + _addDeclaration(e); + } + super.visitSuperConstructorInvocation(node); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + var parent = node.parent; + if (parent is VariableDeclarationList) { + var type = parent.type; + if (type != null) { + type.accept(this); + } + } + super.visitVariableDeclaration(node); + } + + /// Adds the declaration of the top-level element which contains [element] to + /// [declarations], if it is found in [declarationMap]. + /// + /// Also adds the declaration of [element] if it is a public static accessor + /// or static method on a public top-level element. + void _addDeclaration(Element element) { + // First add the enclosing top-level declaration. + var enclosingTopLevelElement = element.thisOrAncestorMatching((a) => + a.enclosingElement == null || + a.enclosingElement is CompilationUnitElement); + var enclosingTopLevelDeclaration = declarationMap[enclosingTopLevelElement]; + if (enclosingTopLevelDeclaration != null) { + declarations.add(enclosingTopLevelDeclaration); + } + + // Also add [element]'s declaration if it is a constructor, static accessor, + // or static method. + if (element.isPrivate) { + return; + } + var enclosingElement = element.enclosingElement; + if (enclosingElement == null || enclosingElement.isPrivate) { + return; + } + if (enclosingElement is InterfaceElement || + enclosingElement is ExtensionElement) { + var declarationElement = element.declaration; + var declaration = declarationMap[declarationElement]; + if (declaration != null) { + declarations.add(declaration); + } + } + } + + void _addDefaultSuperConstructorDeclaration(ClassDeclaration class_) { + var classElement = class_.declaredElement; + var supertype = classElement?.supertype; + if (supertype != null) { + var unnamedConstructor = + supertype.constructors.firstWhereOrNull((e) => e.name.isEmpty); + if (unnamedConstructor != null) { + _addDeclaration(unnamedConstructor); + } + } + } + + void _addNamedType(NamedType? node) { + if (node == null) { + return; + } + + var element = node.element; + if (element == null) { + return; + } + + var declaration = declarationMap[element]; + if (declaration == null) { + return; + } + + declarations.add(declaration); + } + + void _addNamedTypes(List? nodes) { + nodes?.forEach(_addNamedType); + } + + void _visitCompoundAssignmentExpression(CompoundAssignmentExpression node) { + var readElement = node.readElement; + if (readElement != null) { + _addDeclaration(readElement); + } + var writeElement = node.writeElement; + if (writeElement != null) { + _addDeclaration(writeElement); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCompilationUnit(CompilationUnit node) { + var declarationGatherer = _DeclarationGatherer( + linterContext: context, + ); + for (var unit in context.allUnits) { + declarationGatherer.addDeclarations(unit.unit); + } + var declarations = declarationGatherer.declarations; + var entryPoints = declarations.where(_isEntryPoint); + if (entryPoints.isEmpty) return; + + // Map each top-level and static element to its declaration. + var declarationByElement = {}; + for (var declaration in declarations) { + var element = declaration.declaredElement; + if (element != null) { + declarationByElement[element] = declaration; + if (element is TopLevelVariableElement) { + var getter = element.getter; + if (getter != null) declarationByElement[getter] = declaration; + var setter = element.setter; + if (setter != null) declarationByElement[setter] = declaration; + } else if (element is FieldElement) { + var getter = element.getter; + if (getter != null) declarationByElement[getter] = declaration; + var setter = element.setter; + if (setter != null) declarationByElement[setter] = declaration; + } + } + } + + // The set of the declarations which each top-level and static declaration + // references. + var dependencies = >{}; + + // Map each declaration to the collection of declarations which are + // referenced within its body. + for (var declaration in declarations) { + var visitor = _ReferenceVisitor(declarationByElement); + declaration.accept(visitor); + dependencies[declaration] = visitor.declarations; + } + + var usedMembers = entryPoints.toSet(); + var declarationsToCheck = Queue.of(usedMembers); + + // Loop through declarations which are reachable from the set of + // entry-points. We mark each such declaration as "used", and add its + // dependencies to the queue to loop through. Once the queue is empty, + // `usedMembers` contains every declaration reachable from an entry-point. + while (declarationsToCheck.isNotEmpty) { + var declaration = declarationsToCheck.removeLast(); + for (var dep in dependencies[declaration]!) { + if (usedMembers.add(dep)) { + declarationsToCheck.add(dep); + } + } + } + + var unusedDeclarations = declarations.difference(usedMembers); + var unusedMembers = unusedDeclarations.where((declaration) { + var element = declaration.declaredElement; + return element != null && + element.isPublic && + !element.hasVisibleForTesting; + }).toList(); + + for (var member in unusedMembers) { + if (member is ConstructorDeclaration) { + if (member.name == null) { + rule.reportLint(member.returnType, arguments: [member.nameForError]); + } else { + rule.reportLintForToken(member.name, + arguments: [member.nameForError]); + } + } else if (member is NamedCompilationUnitMember) { + rule.reportLintForToken(member.name, arguments: [member.nameForError]); + } else if (member is MethodDeclaration) { + rule.reportLintForToken(member.name, arguments: [member.name.lexeme]); + } else if (member is VariableDeclaration) { + rule.reportLintForToken(member.name, arguments: [member.nameForError]); + } else if (member is ExtensionDeclaration) { + var name = member.name; + rule.reportLintForToken( + name ?? member.extensionKeyword, + arguments: [name?.lexeme ?? ''], + ); + } else { + throw UnimplementedError('(${member.runtimeType}) $member'); + } + } + } + + bool _isEntryPoint(Declaration e) => + e is FunctionDeclaration && + (e.name.lexeme == 'main' || e.metadata.any(_isPragmaVmEntry)); + + bool _isPragmaVmEntry(Annotation annotation) { + if (!annotation.isPragma) return false; + var value = annotation.elementAnnotation?.computeConstantValue(); + if (value == null) return false; + var name = value.getField('name'); + return name != null && + name.hasKnownValue && + name.toStringValue() == 'vm:entry-point'; + } +} + +extension on Element { + bool get isPragma => (library?.isDartCore ?? false) && name == 'pragma'; +} + +extension on Annotation { + bool get isPragma { + var element = elementAnnotation?.element; + DartType type; + if (element is ConstructorElement) { + type = element.returnType; + } else if (element is PropertyAccessorElement && element.isGetter) { + type = element.returnType; + } else { + // Dunno what this is. + return false; + } + return type is InterfaceType && type.element.isPragma; + } +} + +extension on Declaration { + String get nameForError { + // TODO(srawlins): Move this to analyzer when other uses are found. + var self = this; + if (self is ConstructorDeclaration) { + var name = self.name?.lexeme ?? 'new'; + return '${self.returnType.name}.$name'; + } else if (self is EnumConstantDeclaration) { + return self.name.lexeme; + } else if (self is ExtensionDeclaration) { + var name = self.name; + return name?.lexeme ?? 'the unnamed extension'; + } else if (self is MethodDeclaration) { + return self.name.lexeme; + } else if (self is NamedCompilationUnitMember) { + return self.name.lexeme; + } else if (self is VariableDeclaration) { + return self.name.lexeme; + } + + assert(false, 'Uncovered Declaration subtype: ${self.runtimeType}'); + return ''; + } +} + +extension on NamedType { + TypeAnnotation get topmostTypeAnnotation { + TypeAnnotation topTypeAnnotation = this; + var parent = this.parent; + while (parent is TypeAnnotation) { + topTypeAnnotation = parent; + parent = topTypeAnnotation.parent; + } + return topTypeAnnotation; + } + + bool get isInExternalVariableTypeOrFunctionReturnType { + var topTypeAnnotation = topmostTypeAnnotation; + + switch (topTypeAnnotation.parent) { + case MethodDeclaration(:var externalKeyword, :var returnType): + return externalKeyword != null && returnType == topTypeAnnotation; + case VariableDeclarationList( + parent: FieldDeclaration(:var externalKeyword), + ): + case VariableDeclarationList( + parent: TopLevelVariableDeclaration(:var externalKeyword), + ): + return externalKeyword != null; + } + return false; + } +} diff --git a/pkg/linter/lib/src/rules/unrelated_type_equality_checks.dart b/pkg/linter/lib/src/rules/unrelated_type_equality_checks.dart new file mode 100644 index 00000000000..6e7a5583926 --- /dev/null +++ b/pkg/linter/lib/src/rules/unrelated_type_equality_checks.dart @@ -0,0 +1,244 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; +import '../util/dart_type_utilities.dart'; + +const _desc = + r'Equality operator `==` invocation with references of unrelated types.'; + +const _details = r''' +**DON'T** Compare references of unrelated types for equality. + +Comparing references of a type where neither is a subtype of the other most +likely will return `false` and might not reflect programmer's intent. + +`Int64` and `Int32` from `package:fixnum` allow comparing to `int` provided +the `int` is on the right hand side. The lint allows this as a special case. + +**BAD:** +```dart +void someFunction() { + var x = '1'; + if (x == 1) print('someFunction'); // LINT +} +``` + +**BAD:** +```dart +void someFunction1() { + String x = '1'; + if (x == 1) print('someFunction1'); // LINT +} +``` + +**BAD:** +```dart +void someFunction13(DerivedClass2 instance) { + var other = DerivedClass3(); + + if (other == instance) print('someFunction13'); // LINT +} + +class ClassBase {} + +class DerivedClass1 extends ClassBase {} + +abstract class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} +``` + +**GOOD:** +```dart +void someFunction2() { + var x = '1'; + var y = '2'; + if (x == y) print(someFunction2); // OK +} +``` + +**GOOD:** +```dart +void someFunction3() { + for (var i = 0; i < 10; i++) { + if (i == 0) print(someFunction3); // OK + } +} +``` + +**GOOD:** +```dart +void someFunction4() { + var x = '1'; + if (x == null) print(someFunction4); // OK +} +``` + +**GOOD:** +```dart +void someFunction7() { + List someList; + + if (someList.length == 0) print('someFunction7'); // OK +} +``` + +**GOOD:** +```dart +void someFunction8(ClassBase instance) { + DerivedClass1 other; + + if (other == instance) print('someFunction8'); // OK +} +``` + +**GOOD:** +```dart +void someFunction10(unknown) { + var what = unknown - 1; + for (var index = 0; index < unknown; index++) { + if (what == index) print('someFunction10'); // OK + } +} +``` + +**GOOD:** +```dart +void someFunction11(Mixin instance) { + var other = DerivedClass2(); + + if (other == instance) print('someFunction11'); // OK + if (other != instance) print('!someFunction11'); // OK +} + +class ClassBase {} + +abstract class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} +``` + +'''; + +class UnrelatedTypeEqualityChecks extends LintRule { + static const LintCode expressionCode = LintCode( + 'unrelated_type_equality_checks', + uniqueName: 'LintCode.unrelated_type_equality_checks_expression', + "The type of the right operand ('{0}') isn't a subtype or a supertype of " + "the left operand ('{1}').", + correctionMessage: 'Try changing one or both of the operands.'); + + static const LintCode patternCode = LintCode( + 'unrelated_type_equality_checks', + uniqueName: 'LintCode.unrelated_type_equality_checks_pattern', + "The type of the operand ('{0}') isn't a subtype or a supertype of the " + "value being matched ('{1}').", + correctionMessage: 'Try changing one or both of the operands.'); + + UnrelatedTypeEqualityChecks() + : super( + name: 'unrelated_type_equality_checks', + description: _desc, + details: _details, + group: Group.errors); + + @override + List get lintCodes => [expressionCode, patternCode]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context.typeSystem); + registry.addBinaryExpression(this, visitor); + registry.addRelationalPattern(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final TypeSystem typeSystem; + + _Visitor(this.rule, this.typeSystem); + + @override + void visitBinaryExpression(BinaryExpression node) { + var isDartCoreBoolean = node.staticType?.isDartCoreBool ?? false; + if (!isDartCoreBoolean || !node.operator.isEqualityTest) { + return; + } + + var leftOperand = node.leftOperand; + if (leftOperand is NullLiteral) return; + var rightOperand = node.rightOperand; + if (rightOperand is NullLiteral) return; + var leftType = leftOperand.staticType; + if (leftType == null) return; + var rightType = rightOperand.staticType; + if (rightType == null) return; + + if (_nonComparable(leftType, rightType)) { + rule.reportLintForToken( + node.operator, + errorCode: UnrelatedTypeEqualityChecks.expressionCode, + arguments: [ + rightType.getDisplayString(withNullability: true), + leftType.getDisplayString(withNullability: true), + ], + ); + } + } + + @override + void visitRelationalPattern(RelationalPattern node) { + var valueType = node.matchedValueType; + if (valueType == null) return; + if (!node.operator.isEqualityTest) return; + var operandType = node.operand.staticType; + if (operandType == null) return; + if (_nonComparable(valueType, operandType)) { + rule.reportLint( + node, + errorCode: UnrelatedTypeEqualityChecks.patternCode, + arguments: [ + operandType.getDisplayString(withNullability: true), + valueType.getDisplayString(withNullability: true), + ], + ); + } + } + + bool _nonComparable(DartType leftType, DartType rightType) => + typesAreUnrelated(typeSystem, leftType, rightType) && + !(leftType.isFixnumIntX && rightType.isCoreInt); +} + +extension on DartType? { + bool get isCoreInt => this != null && this!.isDartCoreInt; + + bool get isFixnumIntX { + var self = this; + // todo(pq): add tests that ensure this predicate works with fixnum >= 1.1.0-dev + // See: https://github.com/dart-lang/linter/issues/3868 + if (self is! InterfaceType) return false; + var element = self.element; + if (element.name != 'Int32' && element.name != 'Int64') return false; + var uri = element.library.source.uri; + if (!uri.isScheme('package')) return false; + return uri.pathSegments.firstOrNull == 'fixnum'; + } +} + +extension on Token { + bool get isEqualityTest => + type == TokenType.EQ_EQ || type == TokenType.BANG_EQ; +} diff --git a/pkg/linter/lib/src/rules/unsafe_html.dart b/pkg/linter/lib/src/rules/unsafe_html.dart new file mode 100644 index 00000000000..96d56932089 --- /dev/null +++ b/pkg/linter/lib/src/rules/unsafe_html.dart @@ -0,0 +1,211 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = '$_descPrefix.'; +const _descPrefix = r'Avoid unsafe HTML APIs'; + +const _details = r''' +**AVOID** + +* assigning directly to the `href` field of an AnchorElement +* assigning directly to the `src` field of an EmbedElement, IFrameElement, or + ScriptElement +* assigning directly to the `srcdoc` field of an IFrameElement +* calling the `createFragment` method of Element +* calling the `open` method of Window +* calling the `setInnerHtml` method of Element +* calling the `Element.html` constructor +* calling the `DocumentFragment.html` constructor + + +**BAD:** +```dart +var script = ScriptElement()..src = 'foo.js'; +``` +'''; + +class UnsafeHtml extends LintRule { + // TODO(brianwilkerson) These lint codes aren't being used by the lint, but + // are being used to pass the test that ensures that all lint rules define + // their own codes. We would like to use the codes in the future, but doing + // so requires coordination with other tool teams. + static const LintCode attributeCode = LintCode( + 'unsafe_html', "Assigning to the attribute '{0}' is unsafe.", + correctionMessage: 'Try finding a different way to implement the page.', + uniqueName: 'LintCode.unsafe_html_attribute'); + + static const LintCode methodCode = LintCode( + 'unsafe_html', "Invoking the method '{0}' is unsafe.", + correctionMessage: 'Try finding a different way to implement the page.', + uniqueName: 'LintCode.unsafe_html_method'); + + static const LintCode constructorCode = LintCode( + 'unsafe_html', "Invoking the constructor '{0}' is unsafe.", + correctionMessage: 'Try finding a different way to implement the page.', + uniqueName: 'LintCode.unsafe_html_constructor'); + + UnsafeHtml() + : super( + name: 'unsafe_html', + description: _desc, + details: _details, + group: Group.errors); + + @override + List get lintCodes => [ + _Visitor.unsafeAttributeCode, + _Visitor.unsafeMethodCode, + _Visitor.unsafeConstructorCode + ]; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addAssignmentExpression(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + // TODO(srawlins): Reference attributes ('href', 'src', and 'srcdoc') with + // single-quotes to match the convention in the analyzer and linter packages. + // This requires some coordination within Google, as various allow-lists are + // keyed on the exact text of the LintCode message. + // Proposed replacements are commented out in `UnsafeHtml`. + static const unsafeAttributeCode = SecurityLintCode( + 'unsafe_html', + '$_descPrefix (assigning "{0}" attribute).', + uniqueName: 'LintCode.unsafe_html_attribute', + ); + static const unsafeMethodCode = SecurityLintCode( + 'unsafe_html', + "$_descPrefix (calling the '{0}' method of {1}).", + uniqueName: 'LintCode.unsafe_html_method', + ); + static const unsafeConstructorCode = SecurityLintCode( + 'unsafe_html', + "$_descPrefix (calling the '{0}' constructor of {1}).", + uniqueName: 'LintCode.unsafe_html_constructor', + ); + + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + var leftPart = node.leftHandSide.unParenthesized; + if (leftPart is SimpleIdentifier) { + var leftPartElement = node.writeElement; + if (leftPartElement == null) return; + var enclosingElement = leftPartElement.enclosingElement; + if (enclosingElement is ClassElement) { + _checkAssignment(enclosingElement.thisType, leftPart, node); + } + } else if (leftPart is PropertyAccess) { + _checkAssignment( + leftPart.realTarget.staticType, leftPart.propertyName, node); + } else if (leftPart is PrefixedIdentifier) { + _checkAssignment(leftPart.prefix.staticType, leftPart.identifier, node); + } + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var type = node.staticType; + if (type == null) return; + + var constructorName = node.constructorName; + if (constructorName.name?.name == 'html') { + if (type.extendsDartHtmlClass('DocumentFragment')) { + rule.reportLint(node, + arguments: ['html', 'DocumentFragment'], + errorCode: unsafeConstructorCode); + } else if (type.extendsDartHtmlClass('Element')) { + rule.reportLint(node, + arguments: ['html', 'Element'], errorCode: unsafeConstructorCode); + } + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var methodName = node.methodName.name; + + // The static type of the target. + DartType? type; + if (node.realTarget == null) { + // Implicit `this` target. + var methodElement = node.methodName.staticElement; + if (methodElement == null) return; + var enclosingElement = methodElement.enclosingElement; + if (enclosingElement is ClassElement) { + type = enclosingElement.thisType; + } else { + return; + } + } else { + type = node.realTarget?.staticType; + if (type == null) return; + } + + if (methodName == 'createFragment' && + (type is DynamicType || type.extendsDartHtmlClass('Element'))) { + rule.reportLint(node, + arguments: ['createFragment', 'Element'], + errorCode: unsafeMethodCode); + } else if (methodName == 'setInnerHtml' && + (type is DynamicType || type.extendsDartHtmlClass('Element'))) { + rule.reportLint(node, + arguments: ['setInnerHtml', 'Element'], errorCode: unsafeMethodCode); + } else if (methodName == 'open' && + (type is DynamicType || type.extendsDartHtmlClass('Window'))) { + rule.reportLint(node, + arguments: ['open', 'Window'], errorCode: unsafeMethodCode); + } + } + + void _checkAssignment(DartType? type, SimpleIdentifier property, + AssignmentExpression assignment) { + if (type == null) return; + + // It is more efficient to check the setter's name before checking whether + // the target is an interesting type. + if (property.name == 'href') { + if (type is DynamicType || type.extendsDartHtmlClass('AnchorElement')) { + rule.reportLint(assignment, + arguments: ['href'], errorCode: unsafeAttributeCode); + } + } else if (property.name == 'src') { + if (type is DynamicType || + type.extendsDartHtmlClass('EmbedElement') || + type.extendsDartHtmlClass('IFrameElement') || + type.extendsDartHtmlClass('ScriptElement')) { + rule.reportLint(assignment, + arguments: ['src'], errorCode: unsafeAttributeCode); + } + } else if (property.name == 'srcdoc') { + if (type is DynamicType || type.extendsDartHtmlClass('IFrameElement')) { + rule.reportLint(assignment, + arguments: ['srcdoc'], errorCode: unsafeAttributeCode); + } + } + } +} + +extension on DartType? { + /// Returns whether this type extends [className] from the dart:html library. + bool extendsDartHtmlClass(String className) => + extendsClass(className, 'dart.dom.html'); +} diff --git a/pkg/linter/lib/src/rules/use_build_context_synchronously.dart b/pkg/linter/lib/src/rules/use_build_context_synchronously.dart new file mode 100644 index 00000000000..8e985294319 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_build_context_synchronously.dart @@ -0,0 +1,977 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Do not use BuildContexts across async gaps.'; + +const _details = r''' +**DON'T** use BuildContext across asynchronous gaps. + +Storing `BuildContext` for later usage can easily lead to difficult to diagnose +crashes. Asynchronous gaps are implicitly storing `BuildContext` and are some of +the easiest to overlook when writing code. + +When a `BuildContext` is used, its `mounted` property must be checked after an +asynchronous gap. + +**BAD:** +```dart +void onButtonTapped(BuildContext context) async { + await Future.delayed(const Duration(seconds: 1)); + Navigator.of(context).pop(); +} +``` + +**GOOD:** +```dart +void onButtonTapped(BuildContext context) { + Navigator.of(context).pop(); +} +``` + +**GOOD:** +```dart +void onButtonTapped() async { + await Future.delayed(const Duration(seconds: 1)); + + if (!context.mounted) return; + Navigator.of(context).pop(); +} +``` +'''; + +class UseBuildContextSynchronously extends LintRule { + static const LintCode code = LintCode('use_build_context_synchronously', + "Don't use 'BuildContext's across async gaps.", + correctionMessage: + "Try rewriting the code to not reference the 'BuildContext'."); + + /// Flag to short-circuit `inTestDir` checking when running tests. + final bool inTestMode; + + UseBuildContextSynchronously({this.inTestMode = false}) + : super( + name: 'use_build_context_synchronously', + description: _desc, + details: _details, + group: Group.errors, + state: State.experimental(), + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var unit = context.currentUnit.unit; + if (inTestMode || !context.inTestDir(unit)) { + var visitor = _Visitor(this); + registry.addMethodInvocation(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + registry.addFunctionExpressionInvocation(this, visitor); + registry.addPrefixedIdentifier(this, visitor); + } + } +} + +/// An enum whose values describe the state of asynchrony that a certain node +/// has in the syntax tree, with respect to another node. +/// +/// A mounted check is a check of whether a bool-typed identifier, 'mounted', +/// is checked to be `true` or `false`, in a position which affects control +/// flow. +enum AsyncState { + /// A value indicating that a node contains an "asynchronous gap" which is + /// not definitely guarded with a mounted check. + asynchronous, + + /// A value indicating that a node contains a positive mounted check that can + /// guard a certain other node. + mountedCheck, + + /// A value indicating that a node contains a negative mounted check that can + /// guard a certain other node. + notMountedCheck; + + AsyncState? get asynchronousOrNull => + this == asynchronous ? asynchronous : null; +} + +/// A class that reuses a single [AsyncStateVisitor] to calculate and cache the +/// async state between parent and child nodes. +class AsyncStateTracker { + final _asyncStateVisitor = AsyncStateVisitor(); + + /// Returns the asynchronous state that exists between `this` and [reference]. + /// + /// [reference] must be a direct child of `this`, or a sibling of `this` + /// in a List of [AstNode]s. + AsyncState? asyncStateFor(AstNode reference) { + _asyncStateVisitor.reference = reference; + var parent = reference.parent; + if (parent == null) return null; + + var state = parent.accept(_asyncStateVisitor); + _asyncStateVisitor.cacheState(parent, state); + return state; + } +} + +/// A visitor whose `visit*` methods return the async state between a given node +/// and [reference]. +/// +/// The entrypoint for this visitor is [AsyncStateTracker.asyncStateFor]. +/// +/// Each `visit*` method can return one of three values: +/// * `null` means there is no interesting asynchrony between node and +/// [reference]. +/// * [AsyncState.asynchronous] means the node contains an asynchronous gap +/// which is not guarded with a mounted check. +/// * [AsyncState.mountedCheck] means the node guards [reference] with a +/// positive mounted check. +/// * [AsyncState.notMountedCheck] means the node guards [reference] with a +/// negative mounted check. +/// +/// (For all `visit*` methods except the entrypoint call, the value is +/// intermediate, and is only used in calculating the value for parent nodes.) +/// +/// A node that contains a mounted check "guards" [reference] if control flow +/// can only reach [reference] if 'mounted' is `true`. Such checks can take many +/// forms: +/// +/// * A mounted check in an if-condition can be a simple guard for nodes in the +/// if's then-statement or the if's else-statement, depending on the polarity +/// of the check. So `if (mounted) { reference; }` has a proper mounted check +/// and `if (!mounted) {} else { reference; }` has a proper mounted check. +/// * A statement in a series of statements containing a mounted check can guard +/// the later statements if control flow definitely exits in the case of a +/// `false` value for 'mounted'. So `if (!mounted) { return; } reference;` has +/// a proper mounted check. +/// * A mounted check in a try-statement can only guard later statements if it +/// is found in the `finally` section, as no statements found in the `try` +/// section or any `catch` sections are not guaranteed to have run before the +/// later statements. +/// * etc. +/// +/// The `visit*` methods generally fall into three categories: +/// +/// * A node may affect control flow, such that a contained mounted check may +/// properly guard [reference]. See [visitIfStatement] for one of the most +/// complicated examples. +/// * A node may be one component of a mounted check. An associated `visit*` +/// method builds up such a mounted check from inner expressions. For example, +/// given `!(context.mounted)`, the notion of a mounted check is built from +/// the PrefixedIdentifier, the ParenthesizedExpression, and the +/// PrefixExpression (from inside to outside). +/// * Otherwise, a node may just contain an asynchronous gap. The vast majority +/// of node types fall into this category. Most of these `visit*` methods +/// use [AsyncState.asynchronousOrNull] or [_asynchronousIfAnyIsAsync]. +class AsyncStateVisitor extends SimpleAstVisitor { + static const mountedName = 'mounted'; + + late AstNode reference; + + final Map _stateCache = {}; + + AsyncStateVisitor(); + + /// Cache the async state between [node] and some reference node. + /// + /// Caching an async state is only valid when [node] is the parent of the + /// reference node, and later visitations are performed using ancestors of the + /// reference node as [reference]. + /// That is, if the async state between a parent node and a reference node, + /// `R` is `A`, then the async state between any other node and a direct + /// child, which is an ancestor of `R`, is also `A`. + // TODO(srawlins): Checking the cache in every visit method could improve + // performance. Just need to do the legwork. + void cacheState(AstNode node, AsyncState? state) { + _stateCache[node] = state; + } + + @override + AsyncState? visitAdjacentStrings(AdjacentStrings node) => + _asynchronousIfAnyIsAsync(node.strings); + + @override + AsyncState? visitAsExpression(AsExpression node) => + node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitAssignmentExpression(AssignmentExpression node) => + _inOrderAsyncState([ + (node: node.leftHandSide, mountedCanGuard: false), + (node: node.rightHandSide, mountedCanGuard: true), + ]); + + @override + AsyncState? visitAwaitExpression(AwaitExpression node) { + if (_stateCache.containsKey(node)) { + return _stateCache[node]; + } + + // An expression _inside_ an await is executed before the await, and so is + // safe; otherwise asynchronous. + return reference == node.expression ? null : AsyncState.asynchronous; + } + + @override + AsyncState? visitBinaryExpression(BinaryExpression node) { + if (node.leftOperand == reference) { + return null; + } else if (node.rightOperand == reference) { + var leftGuardState = node.leftOperand.accept(this); + return switch (leftGuardState) { + AsyncState.asynchronous => AsyncState.asynchronous, + AsyncState.mountedCheck when node.isAnd => AsyncState.mountedCheck, + AsyncState.notMountedCheck when node.isOr => AsyncState.notMountedCheck, + _ => null, + }; + } + + // `reference` follows `node`, or an ancestor of `node`. + + if (node.isAnd) { + var leftGuardState = node.leftOperand.accept(this); + var rightGuardState = node.rightOperand.accept(this); + return switch ((leftGuardState, rightGuardState)) { + // If the left is uninteresting, just return the state of the right. + (null, _) => rightGuardState, + // If the right is uninteresting, just return the state of the left. + (_, null) => leftGuardState, + // Anything on the left followed by async on the right is async. + (_, AsyncState.asynchronous) => AsyncState.asynchronous, + // An async state on the left is superseded by the state on the right. + (AsyncState.asynchronous, _) => rightGuardState, + // Otherwise just use the state on the left. + (AsyncState.mountedCheck, _) => AsyncState.mountedCheck, + (AsyncState.notMountedCheck, _) => AsyncState.notMountedCheck, + }; + } else if (node.isOr) { + var leftGuardState = node.leftOperand.accept(this); + var rightGuardState = node.rightOperand.accept(this); + return switch ((leftGuardState, rightGuardState)) { + // Anything on the left followed by async on the right is async. + (_, AsyncState.asynchronous) => AsyncState.asynchronous, + // Async on the left followed by anything on the right is async. + (AsyncState.asynchronous, _) => AsyncState.asynchronous, + // A mounted guard only applies if both sides are guarded. + (AsyncState.mountedCheck, AsyncState.mountedCheck) => + AsyncState.mountedCheck, + (AsyncState.notMountedCheck, _) => AsyncState.notMountedCheck, + // Otherwise it's just uninteresting. + (_, _) => null, + }; + } else { + // Outside of a binary logical operation, a mounted check cannot guard a + // later expression, so only check for asynchronous code. + return node.leftOperand.accept(this)?.asynchronousOrNull ?? + node.rightOperand.accept(this)?.asynchronousOrNull; + } + } + + @override + AsyncState? visitBlock(Block node) => + _visitBlockLike(node.statements, parent: node.parent); + + @override + AsyncState? visitBlockFunctionBody(BlockFunctionBody node) => + // Stop visiting when we arrive at a function body. + // Awaits and mounted checks inside it don't matter. + null; + + @override + AsyncState? visitCascadeExpression(CascadeExpression node) => + _asynchronousIfAnyIsAsync([node.target, ...node.cascadeSections]); + + @override + AsyncState? visitCatchClause(CatchClause node) => + node.body.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitConditionalExpression(ConditionalExpression node) => + _visitIfLike( + condition: node.condition, + thenBranch: node.thenExpression, + elseBranch: node.elseExpression, + ); + + @override + AsyncState? visitDoStatement(DoStatement node) { + if (node.body == reference) { + // After one loop, an `await` in the condition can affect the body. + return node.condition.accept(this)?.asynchronousOrNull; + } else if (node.condition == reference) { + // TODO(srawlins): The repetition gets tricky. In this code: + // `do print('hi') while (await f(context));`, the `await` is not unsafe + // for `f(context)` when just looking at the condition without looking at + // the context of the do-statement. However, as the code can loop, the + // `await` _is_ unsafe. It can unwrap to + // `print('hi'); await f(context); print('hi'); await f(context);`. + return node.body.accept(this)?.asynchronousOrNull; + } else { + return node.condition.accept(this)?.asynchronousOrNull ?? + node.body.accept(this)?.asynchronousOrNull; + } + } + + @override + AsyncState? visitExpressionFunctionBody(ExpressionFunctionBody node) => + // Stop visiting when we arrive at a function body. + // Awaits and mounted checks inside it don't matter. + null; + + @override + AsyncState? visitExpressionStatement(ExpressionStatement node) => + node.expression == reference + ? null + : node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitExtensionOverride(ExtensionOverride node) => + _asynchronousIfAnyIsAsync(node.argumentList.arguments); + + @override + AsyncState? visitForElement(ForElement node) { + var forLoopParts = node.forLoopParts; + var referenceIsBody = node.body == reference; + return switch (forLoopParts) { + ForPartsWithDeclarations() => _inOrderAsyncState([ + for (var declaration in forLoopParts.variables.variables) + (node: declaration, mountedCanGuard: false), + (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), + for (var updater in forLoopParts.updaters) + (node: updater, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + ForPartsWithExpression() => _inOrderAsyncState([ + (node: forLoopParts.initialization, mountedCanGuard: false), + (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), + for (var updater in forLoopParts.updaters) + (node: updater, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + ForEachParts() => _inOrderAsyncState([ + (node: forLoopParts.iterable, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + _ => null, + }; + } + + @override + AsyncState? visitForStatement(ForStatement node) { + var forLoopParts = node.forLoopParts; + var referenceIsBody = node.body == reference; + return switch (forLoopParts) { + ForPartsWithDeclarations() => _inOrderAsyncState([ + for (var declaration in forLoopParts.variables.variables) + (node: declaration, mountedCanGuard: false), + // The body can be guarded by the condition. + (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), + for (var updater in forLoopParts.updaters) + (node: updater, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + ForPartsWithExpression() => _inOrderAsyncState([ + (node: forLoopParts.initialization, mountedCanGuard: false), + // The body can be guarded by the condition. + (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), + for (var updater in forLoopParts.updaters) + (node: updater, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + ForEachParts() => _inOrderAsyncState([ + (node: forLoopParts.iterable, mountedCanGuard: false), + (node: node.body, mountedCanGuard: false), + ]), + _ => null, + }; + } + + @override + AsyncState? visitFunctionExpressionInvocation( + FunctionExpressionInvocation node) => + _asynchronousIfAnyIsAsync( + [node.function, ...node.argumentList.arguments]); + + @override + AsyncState? visitIfElement(IfElement node) => _visitIfLike( + condition: node.expression, + thenBranch: node.thenElement, + elseBranch: node.elseElement, + ); + + @override + AsyncState? visitIfStatement(IfStatement node) => _visitIfLike( + condition: node.expression, + thenBranch: node.thenStatement, + elseBranch: node.elseStatement, + ); + + @override + AsyncState? visitIndexExpression(IndexExpression node) => + _asynchronousIfAnyIsAsync([node.target, node.index]); + + @override + AsyncState? visitInstanceCreationExpression( + InstanceCreationExpression node) => + _asynchronousIfAnyIsAsync(node.argumentList.arguments); + + @override + AsyncState? visitInterpolationExpression(InterpolationExpression node) => + node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitIsExpression(IsExpression node) => + node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitLabeledStatement(LabeledStatement node) => + node.statement.accept(this); + + @override + AsyncState? visitListLiteral(ListLiteral node) => + _asynchronousIfAnyIsAsync(node.elements); + + @override + AsyncState? visitMapLiteralEntry(MapLiteralEntry node) => + _asynchronousIfAnyIsAsync([node.key, node.value]); + + @override + AsyncState? visitMethodInvocation(MethodInvocation node) => + _asynchronousIfAnyIsAsync([node.target, ...node.argumentList.arguments]); + + @override + AsyncState? visitNamedExpression(NamedExpression node) => + node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitParenthesizedExpression(ParenthesizedExpression node) => + node.expression.accept(this); + + @override + AsyncState? visitPostfixExpression(PostfixExpression node) => + node.operand.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitPrefixedIdentifier(PrefixedIdentifier node) => + node.identifier.name == mountedName ? AsyncState.mountedCheck : null; + + @override + AsyncState? visitPrefixExpression(PrefixExpression node) { + if (node.isNot) { + var guardState = node.operand.accept(this); + return switch (guardState) { + AsyncState.mountedCheck => AsyncState.notMountedCheck, + AsyncState.notMountedCheck => AsyncState.mountedCheck, + _ => guardState, + }; + } else { + return null; + } + } + + @override + AsyncState? visitPropertyAccess(PropertyAccess node) => + node.target?.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitRecordLiteral(RecordLiteral node) => + _asynchronousIfAnyIsAsync(node.fields); + + @override + AsyncState? visitSetOrMapLiteral(SetOrMapLiteral node) => + _asynchronousIfAnyIsAsync(node.elements); + + @override + AsyncState? visitSimpleIdentifier(SimpleIdentifier node) => + node.name == mountedName ? AsyncState.mountedCheck : null; + + @override + AsyncState? visitSpreadElement(SpreadElement node) => + node.expression.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitStringInterpolation(StringInterpolation node) => + _asynchronousIfAnyIsAsync(node.elements); + + @override + AsyncState? visitSwitchCase(SwitchCase node) => + // TODO(srawlins): Handle when `reference` is in one of the statements. + _inOrderAsyncStateGuardable([node.expression, ...node.statements]); + + @override + AsyncState? visitSwitchDefault(SwitchDefault node) => + _inOrderAsyncStateGuardable(node.statements); + + @override + AsyncState? visitSwitchExpression(SwitchExpression node) => + _asynchronousIfAnyIsAsync([node.expression, ...node.cases]); + + @override + AsyncState? visitSwitchExpressionCase(SwitchExpressionCase node) { + if (reference == node.guardedPattern) { + return null; + } + var whenClauseState = node.guardedPattern.whenClause?.accept(this); + if (reference == node.expression) { + if (whenClauseState == AsyncState.asynchronous || + whenClauseState == AsyncState.mountedCheck) { + return whenClauseState; + } + return null; + } + return whenClauseState?.asynchronousOrNull ?? + node.expression.accept(this)?.asynchronousOrNull; + } + + @override + AsyncState? visitSwitchPatternCase(SwitchPatternCase node) { + if (reference == node.guardedPattern) { + return null; + } + var statementsAsyncState = + _visitBlockLike(node.statements, parent: node.parent); + if (statementsAsyncState != null) return statementsAsyncState; + if (node.statements.contains(reference)) { + // Any when-clause in `node` and any fallthrough when-clauses are handled + // in `visitSwitchStatement`. + return null; + } else { + return node.guardedPattern.whenClause?.accept(this)?.asynchronousOrNull; + } + } + + @override + AsyncState? visitSwitchStatement(SwitchStatement node) { + // TODO(srawlins): Check for definite exits in the members. + node.expression.accept(this)?.asynchronousOrNull ?? + _asynchronousIfAnyIsAsync(node.members); + + var reference = this.reference; + if (reference is SwitchMember) { + var index = node.members.indexOf(reference); + + // Control may flow to `node.statements` via this case's `guardedPattern`, + // or via fallthrough. Consider fallthrough when-clauses. + + // Track whether we are iterating in fall-through cases. + var checkedCasesFallThrough = true; + // Track whether all checked cases have been `AsyncState.mountedCheck` + // (only relevant for fall-through cases). + var checkedCasesAreAllMountedChecks = true; + + for (var i = index; i >= 0; i--) { + var case_ = node.members[i]; + if (case_ is! SwitchPatternCase) { + continue; + } + + var whenAsyncState = case_.guardedPattern.whenClause?.accept(this); + if (whenAsyncState == AsyncState.asynchronous) { + return AsyncState.asynchronous; + } + if (checkedCasesFallThrough) { + var caseIsFallThrough = i == index || case_.statements.isEmpty; + + if (caseIsFallThrough) { + checkedCasesAreAllMountedChecks &= + whenAsyncState == AsyncState.mountedCheck; + } else { + // We have collected whether all of the fallthrough cases have + // mounted guards. + if (checkedCasesAreAllMountedChecks) { + return AsyncState.mountedCheck; + } + } + checkedCasesFallThrough &= caseIsFallThrough; + } + } + + if (checkedCasesFallThrough && checkedCasesAreAllMountedChecks) { + return AsyncState.mountedCheck; + } + + return null; + } else { + return node.expression.accept(this)?.asynchronousOrNull ?? + _asynchronousIfAnyIsAsync(node.members); + } + } + + @override + AsyncState? visitTryStatement(TryStatement node) { + if (node.body == reference) { + return null; + } else if (node.catchClauses.any((clause) => clause == reference)) { + return node.body.accept(this)?.asynchronousOrNull; + } else if (node.finallyBlock == reference) { + return _asynchronousIfAnyIsAsync([node.body, ...node.catchClauses]); + } + + // Only statements in the `finally` section of a try-statement can + // sufficiently guard statements following the try-statement. + return node.finallyBlock?.accept(this) ?? + _asynchronousIfAnyIsAsync([node.body, ...node.catchClauses]); + } + + @override + AsyncState? visitVariableDeclaration(VariableDeclaration node) => + node.initializer?.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitVariableDeclarationStatement( + VariableDeclarationStatement node) => + _asynchronousIfAnyIsAsync([ + for (var variable in node.variables.variables) variable.initializer, + ]); + + @override + AsyncState? visitWhenClause(WhenClause node) => node.expression.accept(this); + + @override + AsyncState? visitWhileStatement(WhileStatement node) => + // TODO(srawlins): if the condition is a mounted guard and `reference` is + // the body or follows the while. + // A while-statement's body is not guaranteed to execute, so no mounted + // checks properly guard. + node.condition.accept(this)?.asynchronousOrNull ?? + node.body.accept(this)?.asynchronousOrNull; + + @override + AsyncState? visitYieldStatement(YieldStatement node) => + node.expression.accept(this)?.asynchronousOrNull; + + /// Returns [AsyncState.asynchronous] if visiting any of [nodes] returns + /// [AsyncState.asynchronous], otherwise `null`. + /// + /// This function does not take mounted checks into account, so it cannot be + /// used when [nodes] can affect control flow. + AsyncState? _asynchronousIfAnyIsAsync(List nodes) { + var index = nodes.indexOf(reference); + if (index < 0) { + return nodes.any((node) => node?.accept(this) == AsyncState.asynchronous) + ? AsyncState.asynchronous + : null; + } else { + return nodes + .take(index) + .any((node) => node?.accept(this) == AsyncState.asynchronous) + ? AsyncState.asynchronous + : null; + } + } + + /// Walks backwards through [nodes] looking for "interesting" async states, + /// determining the async state of [nodes], with respect to [reference]. + /// + /// [nodes] is a list of records, each with an [AstNode] and a field + /// representing whether a mounted check in the node can guard [reference]. + /// + /// [nodes] must be in expected execution order. [reference] can be one of + /// [nodes], or can follow [nodes], or can follow an ancestor of [nodes]. + /// + /// If [reference] is one of the [nodes], this traversal starts at the node + /// that precedes it, rather than at the end of the list. + AsyncState? _inOrderAsyncState( + List<({AstNode? node, bool mountedCanGuard})> nodes) { + if (nodes.isEmpty) return null; + if (nodes.first.node == reference) return null; + var referenceIndex = + nodes.indexWhere((element) => element.node == reference); + var startingIndex = + referenceIndex > 0 ? referenceIndex - 1 : nodes.length - 1; + + for (var i = startingIndex; i >= 0; i--) { + var (:node, :mountedCanGuard) = nodes[i]; + if (node == null) continue; + var asyncState = node.accept(this); + if (asyncState == AsyncState.asynchronous) { + return AsyncState.asynchronous; + } + if (mountedCanGuard && asyncState != null) { + // Walking from the last node to the first, as soon as we encounter a + // mounted check (positive or negative) or asynchronous code, that's + // the state of the whole series. + return asyncState; + } + } + return null; + } + + /// A simple wrapper for [_inOrderAsyncState] for [nodes] which can all guard + /// [reference] with a mounted check. + AsyncState? _inOrderAsyncStateGuardable(Iterable nodes) => + _inOrderAsyncState([ + for (var node in nodes) (node: node, mountedCanGuard: true), + ]); + + /// Compute the [AsyncState] of a "block-like" node which has [statements]. + AsyncState? _visitBlockLike(List statements, + {required AstNode? parent}) { + var reference = this.reference; + if (reference is Statement) { + var index = statements.indexOf(reference); + if (index >= 0) { + var precedingAsyncState = _inOrderAsyncStateGuardable(statements); + if (precedingAsyncState != null) return precedingAsyncState; + if (parent is DoStatement || + parent is ForStatement || + parent is WhileStatement) { + // Check for asynchrony in the statements that _follow_ [reference], + // as they may lead to an async gap before we loop back to + // [reference]. + return _inOrderAsyncStateGuardable(statements.skip(index + 1)) + ?.asynchronousOrNull; + } + return null; + } + } + + // When [reference] is not one of [node.statements], walk through all of + // them. + return statements.reversed + .map((s) => s.accept(this)) + .firstWhereOrNull((state) => state != null); + } + + /// Compute the [AsyncState] of an "if-like" node which has a [condition], a + /// [thenBranch], and a possible [elseBranch]. + AsyncState? _visitIfLike({ + required AstNode condition, + required AstNode thenBranch, + required AstNode? elseBranch, + }) { + if (reference == condition) { + return null; + } + var conditionMountedCheck = condition.accept(this); + + if (reference == thenBranch) { + return switch (conditionMountedCheck) { + AsyncState.asynchronous => AsyncState.asynchronous, + AsyncState.mountedCheck => AsyncState.mountedCheck, + _ => null, + }; + } else if (reference == elseBranch) { + return switch (conditionMountedCheck) { + AsyncState.asynchronous => AsyncState.asynchronous, + AsyncState.notMountedCheck => AsyncState.mountedCheck, + _ => null, + }; + } else { + // `reference` is a statement that comes after `node`, or an ancestor of + // `node`, in a NodeList. + var thenAsyncState = thenBranch.accept(this); + var elseAsyncState = elseBranch?.accept(this); + var thenTerminates = thenBranch.terminatesControl; + var elseTerminates = elseBranch?.terminatesControl ?? false; + + if (thenAsyncState == AsyncState.notMountedCheck) { + if (elseAsyncState == AsyncState.notMountedCheck || elseTerminates) { + return AsyncState.notMountedCheck; + } + } + if (elseAsyncState == AsyncState.notMountedCheck && thenTerminates) { + return AsyncState.notMountedCheck; + } + + if (thenAsyncState == AsyncState.asynchronous && !thenTerminates) { + return AsyncState.asynchronous; + } + if (elseAsyncState == AsyncState.asynchronous && !elseTerminates) { + return AsyncState.asynchronous; + } + + if (conditionMountedCheck == AsyncState.asynchronous) { + return AsyncState.asynchronous; + } + + if (conditionMountedCheck == AsyncState.mountedCheck && elseTerminates) { + return AsyncState.notMountedCheck; + } + + if (conditionMountedCheck == AsyncState.notMountedCheck && + thenTerminates) { + return AsyncState.notMountedCheck; + } + + return null; + } + } +} + +class _Visitor extends SimpleAstVisitor { + static const mountedName = 'mounted'; + + final LintRule rule; + + _Visitor(this.rule); + + bool accessesContext(ArgumentList argumentList) => + argumentList.arguments.any((argument) => argument.accessesContext); + + void check(AstNode node) { + /// Checks each of the [statements] before [child] for a `mounted` check, + /// and returns whether it did not find one (and the caller should keep + /// looking). + + // Walk back and look for an async gap that is not guarded by a mounted + // property check. + AstNode? child = node; + var asyncStateTracker = AsyncStateTracker(); + while (child != null && child is! FunctionBody) { + var parent = child.parent; + if (parent == null) break; + + var asyncState = asyncStateTracker.asyncStateFor(child); + if (asyncState == AsyncState.asynchronous) { + rule.reportLint(node); + return; + } else if (asyncState.isGuarded) { + return; + } + + child = parent; + } + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + if (accessesContext(node.argumentList)) { + check(node); + } + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (accessesContext(node.argumentList)) { + check(node); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (isBuildContext(node.target?.staticType, skipNullable: true) || + accessesContext(node.argumentList)) { + check(node); + } + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + if (node.identifier.name == mountedName) { + // Accessing `context.mounted` does not count as a "use" of a + // `BuildContext` which needs to be guarded by a mounted check. + return; + } + // Getter access. + if (isBuildContext(node.prefix.staticType, skipNullable: true)) { + if (node.identifier.name != 'mounted') { + check(node); + } + } + } +} + +extension on AsyncState? { + bool get isGuarded => + this == AsyncState.mountedCheck || this == AsyncState.notMountedCheck; +} + +extension on AstNode { + bool get terminatesControl { + var self = this; + if (self is Block) { + return self.statements.isNotEmpty && + self.statements.last.terminatesControl; + } + // TODO(srawlins): Make ExitDetector 100% functional for our needs. The + // basic (only?) difference is that it doesn't consider a `break` statement + // to be exiting. + if (self is ReturnStatement || + self is BreakStatement || + self is ContinueStatement) { + return true; + } + return accept(ExitDetector()) ?? false; + } +} + +extension on PrefixExpression { + bool get isNot => operator.type == TokenType.BANG; +} + +extension on BinaryExpression { + bool get isAnd => operator.type == TokenType.AMPERSAND_AMPERSAND; + bool get isOr => operator.type == TokenType.BAR_BAR; +} + +extension on Expression { + /// Whether this accesses a `BuildContext`. + bool get accessesContext { + var self = this; + if (self is NamedExpression) { + self = self.expression; + } + if (self is PropertyAccess) { + self = self.propertyName; + } + + if (self is Identifier) { + var element = self.staticElement; + if (element == null) { + return false; + } + + var declaration = element.declaration; + // Get the declaration to ensure checks from un-migrated libraries work. + DartType? argType = switch (declaration) { + ExecutableElement() => declaration.returnType, + VariableElement() => declaration.type, + _ => null, + }; + + var isGetter = element is PropertyAccessorElement; + return isBuildContext(argType, skipNullable: isGetter); + } else if (self is ParenthesizedExpression) { + return self.expression.accessesContext; + } else if (self is PostfixExpression) { + return self.operator.type == TokenType.BANG && + self.operand.accessesContext; + } + return false; + } +} + +extension on Statement { + /// Whether this statement terminates control, via a [BreakStatement], a + /// [ContinueStatement], or other definite exits, as determined by + /// [ExitDetector]. + bool get terminatesControl { + var self = this; + if (self is Block) { + var last = self.statements.lastOrNull; + return last != null && last.terminatesControl; + } + // TODO(srawlins): Make ExitDetector 100% functional for our needs. The + // basic (only?) difference is that it doesn't consider a `break` statement + // to be exiting. + if (self is BreakStatement || self is ContinueStatement) { + return true; + } + return accept(ExitDetector()) ?? false; + } +} diff --git a/pkg/linter/lib/src/rules/use_colored_box.dart b/pkg/linter/lib/src/rules/use_colored_box.dart new file mode 100644 index 00000000000..1841ccc23d3 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_colored_box.dart @@ -0,0 +1,114 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Use `ColoredBox`.'; + +const _details = r''' +**DO** use `ColoredBox` when `Container` has only a `Color`. + +A `Container` is a heavier Widget than a `ColoredBox`, and as bonus, +`ColoredBox` has a `const` constructor. + +**BAD:** +```dart +Widget buildArea() { + return Container( + color: Colors.blue, + child: const Text('hello'), + ); +} +``` + +**GOOD:** +```dart +Widget buildArea() { + return const ColoredBox( + color: Colors.blue, + child: Text('hello'), + ); +} +``` +'''; + +class UseColoredBox extends LintRule { + static const LintCode code = LintCode('use_colored_box', + "Use a 'ColoredBox' rather than a 'Container' with only a 'Color'.", + correctionMessage: "Try replacing the 'Container' with a 'ColoredBox'."); + + UseColoredBox() + : super( + name: 'use_colored_box', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _ArgumentData { + var positionalArgumentsFound = false; + var additionalArgumentsFound = false; + var hasColor = false; + var hasChild = false; + + _ArgumentData(ArgumentList node) { + for (var argument in node.arguments) { + if (argument is! NamedExpression) { + positionalArgumentsFound = true; + return; + } + var label = argument.name.label; + if (label.name == 'color' && + argument.staticType?.nullabilitySuffix != + NullabilitySuffix.question) { + hasColor = true; + } else if (label.name == 'child') { + hasChild = true; + } else if (label.name == 'key') { + // Ignore key + } else { + additionalArgumentsFound = true; + } + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isExactWidgetTypeContainer(node.staticType)) { + return; + } + + var data = _ArgumentData(node.argumentList); + + if (data.additionalArgumentsFound || data.positionalArgumentsFound) { + return; + } + + if (data.hasChild && data.hasColor) { + rule.reportLint(node.constructorName); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_decorated_box.dart b/pkg/linter/lib/src/rules/use_decorated_box.dart new file mode 100644 index 00000000000..dedc935da01 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_decorated_box.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Use `DecoratedBox`.'; + +const _details = r''' +**DO** use `DecoratedBox` when `Container` has only a `Decoration`. + +A `Container` is a heavier Widget than a `DecoratedBox`, and as bonus, +`DecoratedBox` has a `const` constructor. + +**BAD:** +```dart +Widget buildArea() { + return Container( + decoration: const BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + child: const Text('...'), + ); +} +``` + +**GOOD:** +```dart +Widget buildArea() { + return const DecoratedBox( + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + child: Text('...'), + ); +} +``` +'''; + +class UseDecoratedBox extends LintRule { + static const LintCode code = LintCode('use_decorated_box', + "Use 'DecoratedBox' rather than a 'Container' with only a 'Decoration'.", + correctionMessage: + "Try replacing the 'Container' with a 'DecoratedBox'."); + + UseDecoratedBox() + : super( + name: 'use_decorated_box', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _ArgumentData { + var positionalArgumentsFound = false; + var additionalArgumentsFound = false; + var hasDecoration = false; + var hasChild = false; + + _ArgumentData(ArgumentList node) { + for (var argument in node.arguments) { + if (argument is! NamedExpression) { + positionalArgumentsFound = true; + return; + } + var label = argument.name.label; + if (label.name == 'decoration') { + hasDecoration = true; + } else if (label.name == 'child') { + hasChild = true; + } else if (label.name == 'key') { + // Ignore key + } else { + additionalArgumentsFound = true; + } + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!isExactWidgetTypeContainer(node.staticType)) { + return; + } + + var data = _ArgumentData(node.argumentList); + + if (data.additionalArgumentsFound || data.positionalArgumentsFound) { + return; + } + + if (data.hasChild && data.hasDecoration) { + rule.reportLint(node.constructorName); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_enums.dart b/pkg/linter/lib/src/rules/use_enums.dart new file mode 100644 index 00000000000..8b4fca50739 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_enums.dart @@ -0,0 +1,250 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; + +const _desc = r'Use enums rather than classes that behave like enums.'; + +// todo(pq): add a link to any docs describing enhanced-enums once published + +const _details = r''' +Classes that look like enumerations should be declared as `enum`s. + +**DO** use enums where appropriate. + +Candidates for enums are classes that: + * are concrete, + * are private or have only private generative constructors, + * have two or more static const fields with the same type as the class, + * have generative constructors that are only invoked at the top-level of the + initialization expression of these static fields, + * do not define `hashCode`, `==`, `values` or `index`, + * do not extend any class other than Object, and + * have no subclasses declared in the defining library. + +**BAD:** +```dart +class LogPriority { + static const error = LogPriority._(1, 'Error'); + static const warning = LogPriority._(2, 'Warning'); + static const log = LogPriority._unknown('Log'); + + final String prefix; + final int priority; + const LogPriority._(this.priority, this.prefix); + const LogPriority._unknown(String prefix) : this._(-1, prefix); +} +``` + +**GOOD:** +```dart +enum LogPriority { + error(1, 'Error'), + warning(2, 'Warning'), + log.unknown('Log'); + + final String prefix; + final int priority; + const LogPriority(this.priority, this.prefix); + const LogPriority.unknown(String prefix) : this(-1, prefix); +} +``` +'''; + +class UseEnums extends LintRule { + static const LintCode code = LintCode('use_enums', 'Class should be an enum.', + correctionMessage: 'Try using an enum rather than a class.'); + + UseEnums() + : super( + name: 'use_enums', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.enhanced_enums)) return; + + var visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + } +} + +/// A superclass for the [_EnumVisitor] and [_NonEnumVisitor]. +class _BaseVisitor extends RecursiveAstVisitor { + /// The element representing the enum declaration that's being visited. + final ClassElement classElement; + + _BaseVisitor(this.classElement); + + /// Return `true` if the given [node] is an invocation of a generative + /// constructor from the class being converted. + bool invokesGenerativeConstructor(InstanceCreationExpression node) { + var constructorElement = node.constructorName.staticElement; + return constructorElement != null && + !constructorElement.isFactory && + constructorElement.enclosingElement == classElement; + } +} + +/// A visitor used to visit the class being linted. This visitor throws an +/// exception if a constructor for the class is invoked anywhere other than the +/// top-level expression of an initializer for one of the fields being converted. +/// +/// Note: copied and modified from server. +class _EnumVisitor extends _BaseVisitor { + /// A flag indicating whether we are currently visiting the children of a + /// field declaration that will be converted to be a constant. + bool inConstantDeclaration = false; + + List variableDeclarations; + + _EnumVisitor(super.classElement, this.variableDeclarations); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (!inConstantDeclaration) { + if (invokesGenerativeConstructor(node)) { + throw _InvalidEnumException(); + } + } + inConstantDeclaration = false; + super.visitInstanceCreationExpression(node); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (variableDeclarations.contains(node)) { + inConstantDeclaration = true; + } + super.visitVariableDeclaration(node); + inConstantDeclaration = false; + } +} + +/// Note: copied and modified from server. +class _InvalidEnumException implements Exception {} + +/// A visitor that visits everything in the library other than the class being +/// linted. This visitor throws an exception if +/// - there is a subclass of the class, or +/// - there is an invocation of one of the constructors of the class. +/// +/// Note: copied and modified from server. +class _NonEnumVisitor extends _BaseVisitor { + /// Initialize a newly created visitor to visit everything except the class + /// declaration corresponding to the given [classElement]. + _NonEnumVisitor(super.classElement); + + @override + void visitClassDeclaration(ClassDeclaration node) { + var element = node.declaredElement; + if (element == null) { + throw _InvalidEnumException(); + } + if (element != classElement) { + if (element.supertype?.element == classElement) { + throw _InvalidEnumException(); + } else if (element.interfaces + .map((e) => e.element) + .contains(classElement)) { + throw _InvalidEnumException(); + } else if (element.mixins.map((e) => e.element).contains(classElement)) { + // This case won't occur unless there's an error in the source code, but + // it's easier to check for the condition than it is to check for the + // diagnostic. + throw _InvalidEnumException(); + } + super.visitClassDeclaration(node); + } + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (invokesGenerativeConstructor(node)) { + throw _InvalidEnumException(); + } + super.visitInstanceCreationExpression(node); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + @override + visitClassDeclaration(ClassDeclaration node) { + if (node.abstractKeyword != null) return; + var classElement = node.declaredElement; + if (classElement == null) return; + + // Enums can only extend Object. + if (classElement.supertype?.isDartCoreObject == false) { + return; + } + + var candidateConstants = []; + + for (var member in node.members) { + if (isHashCode(member)) return; + if (isIndex(member)) return; + if (isEquals(member)) return; + if (isValues(member)) return; + + if (member is FieldDeclaration) { + if (!member.isStatic) continue; + for (var field in member.fields.variables) { + var fieldElement = field.declaredElement; + if (fieldElement is! FieldElement) continue; + if (field.isSynthetic || !field.isConst) continue; + var initializer = field.initializer; + if (initializer is! InstanceCreationExpression) continue; + + var constructorElement = initializer.constructorName.staticElement; + if (constructorElement == null) continue; + if (constructorElement.isFactory) continue; + if (constructorElement.enclosingElement != classElement) continue; + + if (fieldElement.computeConstantValue() == null) continue; + + candidateConstants.add(field); + } + } + if (member is ConstructorDeclaration) { + var constructor = member.declaredElement; + if (constructor == null) return; + if (!constructor.isFactory && !constructor.isConst) return; + var name = member.name?.lexeme; + if (classElement.isPublic && + (name == null || !Identifier.isPrivateName(name))) { + return; + } + } + } + + if (candidateConstants.length < 2) return; + + try { + node.accept(_EnumVisitor(classElement, candidateConstants)); + node.root.accept(_NonEnumVisitor(classElement)); + } on _InvalidEnumException { + return; + } + + rule.reportLintForToken(node.name); + } +} diff --git a/pkg/linter/lib/src/rules/use_full_hex_values_for_flutter_colors.dart b/pkg/linter/lib/src/rules/use_full_hex_values_for_flutter_colors.dart new file mode 100644 index 00000000000..a64f30ca77f --- /dev/null +++ b/pkg/linter/lib/src/rules/use_full_hex_values_for_flutter_colors.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Prefer an 8-digit hexadecimal integer(0xFFFFFFFF) to instantiate Color.'; + +const _details = r''' +**PREFER** an 8-digit hexadecimal integer(0xFFFFFFFF) to instantiate Color. Colors +have four 8-bit channels, which adds up to 32 bits, so Colors are described +using a 32 bit integer. + +**BAD:** +```dart +Color(1); +Color(0x000001); +``` + +**GOOD:** +```dart +Color(0x00000001); +``` + +'''; + +class UseFullHexValuesForFlutterColors extends LintRule { + static const LintCode code = LintCode( + 'use_full_hex_values_for_flutter_colors', + "Instances of 'Color' should be created using an 8-digit hexadecimal " + "integer (such as '0xFFFFFFFF').", + correctionMessage: + "Try using an 8-digit hexadecimal integer to create the 'Color'."); + + UseFullHexValuesForFlutterColors() + : super( + name: 'use_full_hex_values_for_flutter_colors', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var element = node.constructorName.staticElement; + if (element != null && + element.isSameAs( + uri: 'dart.ui', className: 'Color', constructorName: '')) { + var arguments = node.argumentList.arguments; + if (arguments.isNotEmpty) { + var argument = arguments.first; + if (argument is IntegerLiteral) { + var value = argument.literal.lexeme.toLowerCase(); + if (!value.startsWith('0x') || value.length != 10) { + rule.reportLint(argument); + } + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/use_function_type_syntax_for_parameters.dart b/pkg/linter/lib/src/rules/use_function_type_syntax_for_parameters.dart new file mode 100644 index 00000000000..691843449ad --- /dev/null +++ b/pkg/linter/lib/src/rules/use_function_type_syntax_for_parameters.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use generic function type syntax for parameters.'; + +const _details = r''' +Use generic function type syntax for parameters. + +**BAD:** +```dart +Iterable where(bool predicate(T element)) {} +``` + +**GOOD:** +```dart +Iterable where(bool Function(T) predicate) {} +``` + +'''; + +class UseFunctionTypeSyntaxForParameters extends LintRule { + @override + bool get canUseParsedResult => true; + + static const LintCode code = LintCode( + 'use_function_type_syntax_for_parameters', + "Use the generic function type syntax to declare the parameter '{0}'.", + correctionMessage: 'Try using the generic function type syntax.'); + + UseFunctionTypeSyntaxForParameters() + : super( + name: 'use_function_type_syntax_for_parameters', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addFunctionTypedFormalParameter(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { + rule.reportLint(node, arguments: [node.name.lexeme]); + } +} diff --git a/pkg/linter/lib/src/rules/use_if_null_to_convert_nulls_to_bools.dart b/pkg/linter/lib/src/rules/use_if_null_to_convert_nulls_to_bools.dart new file mode 100644 index 00000000000..2f2a40029d4 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_if_null_to_convert_nulls_to_bools.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use if-null operators to convert nulls to bools.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#prefer-using--to-convert-null-to-a-boolean-value): + +Use if-null operators to convert nulls to bools. + +**BAD:** +```dart +if (nullableBool == true) { +} +if (nullableBool != false) { +} +``` + +**GOOD:** +```dart +if (nullableBool ?? false) { +} +if (nullableBool ?? true) { +} +``` + +'''; + +class UseIfNullToConvertNullsToBools extends LintRule { + static const LintCode code = LintCode('use_if_null_to_convert_nulls_to_bools', + "Use an if-null operator to convert a 'null' to a 'bool'.", + correctionMessage: 'Try using an if-null operator.'); + + UseIfNullToConvertNullsToBools() + : super( + name: 'use_if_null_to_convert_nulls_to_bools', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.non_nullable)) return; + + var visitor = _Visitor(this, context); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final LinterContext context; + + _Visitor(this.rule, this.context); + + bool isNullableBool(DartType? type) => + type != null && + type.isDartCoreBool && + context.typeSystem.isNullable(type); + + @override + void visitBinaryExpression(BinaryExpression node) { + var type = node.leftOperand.staticType; + var right = node.rightOperand; + if (node.operator.type == TokenType.EQ_EQ && + isNullableBool(type) && + right is BooleanLiteral && + right.value) { + rule.reportLint(node); + } + if (node.operator.type == TokenType.BANG_EQ && + isNullableBool(type) && + right is BooleanLiteral && + !right.value) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_is_even_rather_than_modulo.dart b/pkg/linter/lib/src/rules/use_is_even_rather_than_modulo.dart new file mode 100644 index 00000000000..14cdd8b247a --- /dev/null +++ b/pkg/linter/lib/src/rules/use_is_even_rather_than_modulo.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = + r'Prefer intValue.isOdd/isEven instead of checking the result of % 2.'; + +const _details = r''' +**PREFER** the use of intValue.isOdd/isEven to check for evenness. + +**BAD:** +```dart +bool isEven = 1 % 2 == 0; +bool isOdd = 13 % 2 == 1; +``` + +**GOOD:** +```dart +bool isEven = 1.isEven; +bool isOdd = 13.isOdd; +``` + +'''; + +class UseIsEvenRatherThanModulo extends LintRule { + static const LintCode code = LintCode( + 'use_is_even_rather_than_modulo', "Use '{0}' rather than '% 2'.", + correctionMessage: "Try using '{0}'."); + + UseIsEvenRatherThanModulo() + : super( + name: 'use_is_even_rather_than_modulo', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addBinaryExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + _Visitor(this.rule); + + @override + void visitBinaryExpression(BinaryExpression node) { + // This lint error only happens when the operator is equality. + if (node.operator.type != TokenType.EQ_EQ) { + return; + } + var left = node.leftOperand; + var leftType = left.staticType; + var right = node.rightOperand; + var rightType = right.staticType; + // Both sides have to have static type of int + if (!(right is IntegerLiteral && + (leftType?.isDartCoreInt ?? false) && + (rightType?.isDartCoreInt ?? false))) { + return; + } + // The left side expression has to be modulo by 2 type. + if (left is BinaryExpression) { + var rightChild = left.rightOperand; + var rightChildType = rightChild.staticType; + if (left.operator.type == TokenType.PERCENT && + rightChild is IntegerLiteral && + rightChild.value == 2 && + (rightChildType?.isDartCoreInt ?? false)) { + var value = right.value; + if (value == null) { + return; + } + rule.reportLint(node, arguments: [value == 0 ? 'isEven' : 'isOdd']); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/use_key_in_widget_constructors.dart b/pkg/linter/lib/src/rules/use_key_in_widget_constructors.dart new file mode 100644 index 00000000000..1243772d02d --- /dev/null +++ b/pkg/linter/lib/src/rules/use_key_in_widget_constructors.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; +import '../util/flutter_utils.dart'; + +const _desc = r'Use key in widget constructors.'; + +const _details = r''' +**DO** use key in widget constructors. + +It's a good practice to expose the ability to provide a key when creating public +widgets. + +**BAD:** +```dart +class MyPublicWidget extends StatelessWidget { +} +``` + +**GOOD:** +```dart +class MyPublicWidget extends StatelessWidget { + MyPublicWidget({super.key}); +} +``` +'''; + +class UseKeyInWidgetConstructors extends LintRule { + static const LintCode code = LintCode('use_key_in_widget_constructors', + "Constructors for public widgets should have a named 'key' parameter.", + correctionMessage: 'Try adding a named parameter to the constructor.'); + + UseKeyInWidgetConstructors() + : super( + name: 'use_key_in_widget_constructors', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addClassDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitClassDeclaration(ClassDeclaration node) { + var classElement = node.declaredElement; + if (classElement != null && + classElement.isPublic && + hasWidgetAsAscendant(classElement) && + classElement.constructors.where((e) => !e.isSynthetic).isEmpty) { + rule.reportLintForToken(node.name); + } + super.visitClassDeclaration(node); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + var constructorElement = node.declaredElement; + if (constructorElement == null) { + return; + } + var classElement = constructorElement.enclosingElement; + if (constructorElement.isPublic && + !constructorElement.isFactory && + classElement.isPublic && + classElement is ClassElement && + hasWidgetAsAscendant(classElement) && + !isExactWidget(classElement) && + !_hasKeySuperParameterInitializerArg(node) && + !node.initializers.any((initializer) { + if (initializer is SuperConstructorInvocation) { + var staticElement = initializer.staticElement; + return staticElement != null && + (!_defineKeyParameter(staticElement) || + _defineKeyArgument(initializer.argumentList)); + } else if (initializer is RedirectingConstructorInvocation) { + var staticElement = initializer.staticElement; + return staticElement != null && + (!_defineKeyParameter(staticElement) || + _defineKeyArgument(initializer.argumentList)); + } + return false; + })) { + var errorNode = node.name ?? node.returnType; + rule.reportLintForOffset(errorNode.offset, errorNode.length); + } + super.visitConstructorDeclaration(node); + } + + bool _defineKeyArgument(ArgumentList argumentList) => argumentList.arguments + .any((a) => a.staticParameterElement?.name == 'key'); + + bool _defineKeyParameter(ConstructorElement element) => + element.parameters.any((e) => e.name == 'key' && _isKeyType(e.type)); + + bool _hasKeySuperParameterInitializerArg(ConstructorDeclaration node) { + for (var parameter in node.parameters.parameters) { + var element = parameter.declaredElement; + if (element is SuperFormalParameterElement && element.name == 'key') { + return true; + } + } + + return false; + } + + bool _isKeyType(DartType type) => type.implementsInterface('Key', ''); +} diff --git a/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart b/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart new file mode 100644 index 00000000000..46466186e5a --- /dev/null +++ b/pkg/linter/lib/src/rules/use_late_for_private_fields_and_variables.dart @@ -0,0 +1,267 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use late for private members with a non-nullable type.'; + +const _details = r''' +Use `late` for private members with non-nullable types that are always expected +to be non-null. Thus it's clear that the field is not expected to be `null` +and it avoids null checks. + +**BAD:** +```dart +int? _i; +m() { + _i!.abs(); +} +``` + +**GOOD:** +```dart +late int _i; +m() { + _i.abs(); +} +``` + +**OK:** +```dart +int? _i; +m() { + _i?.abs(); + _i = null; +} +``` + +'''; + +bool _isPrivateExtension(AstNode parent) { + if (parent is! ExtensionDeclaration) { + return false; + } + var parentName = parent.name?.lexeme; + return parentName == null || Identifier.isPrivateName(parentName); +} + +class UseLateForPrivateFieldsAndVariables extends LintRule { + static const LintCode code = LintCode( + 'use_late_for_private_fields_and_variables', + "Use 'late' for private members with a non-nullable type.", + correctionMessage: "Try making adding the modifier 'late'."); + + UseLateForPrivateFieldsAndVariables() + : super( + name: 'use_late_for_private_fields_and_variables', + description: _desc, + details: _details, + state: State.experimental(), + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends RecursiveAstVisitor { + static final lateables = + >{}; + + static final nullableAccess = >{}; + final LintRule rule; + + final LinterContext context; + + CompilationUnitElement? currentUnit; + _Visitor(this.rule, this.context); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + var element = node.writeElement?.canonicalElement; + if (element != null) { + var assignee = node.leftHandSide; + var rhsType = node.rightHandSide.staticType; + if (assignee is SimpleIdentifier && assignee.inDeclarationContext()) { + // This is OK. + } else if (node.operator.type == TokenType.EQ && + rhsType != null && + context.typeSystem.isNonNullable(rhsType)) { + // This is OK; non-null access. + } else { + nullableAccess[currentUnit]?.add(element); + } + } + super.visitAssignmentExpression(node); + } + + @override + void visitClassDeclaration(ClassDeclaration node) { + // See: https://dart.dev/tools/diagnostic-messages#late_final_field_with_const_constructor + for (var member in node.members) { + if (member is ConstructorDeclaration && member.constKeyword != null) { + return; + } + } + + super.visitClassDeclaration(node); + } + + @override + void visitCompilationUnit(CompilationUnit node) { + if (node.featureSet.isEnabled(Feature.non_nullable)) { + var declaredElement = node.declaredElement; + if (declaredElement == null) { + return; + } + lateables.putIfAbsent(declaredElement, () => []); + nullableAccess.putIfAbsent(declaredElement, () => {}); + currentUnit = declaredElement; + + super.visitCompilationUnit(node); + + var unitsInContext = + context.allUnits.map((e) => e.unit.declaredElement).toSet(); + var libraryUnitsInContext = + declaredElement.library.units.where(unitsInContext.contains).toSet(); + var areAllLibraryUnitsVisited = + libraryUnitsInContext.every(lateables.containsKey); + if (areAllLibraryUnitsVisited) { + _checkAccess(libraryUnitsInContext); + + // Clean up. + for (var unit in libraryUnitsInContext) { + lateables.remove(unit); + nullableAccess.remove(unit); + } + } + } + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + // See: https://dart.dev/tools/diagnostic-messages#late_final_field_with_const_constructor + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + var parent = node.parent; + if (parent is ExtensionTypeDeclaration && !node.isStatic) return; + if (parent != null) { + var parentIsPrivateExtension = _isPrivateExtension(parent); + for (var variable in node.fields.variables) { + // See + // https://github.com/dart-lang/linter/pull/2189#issuecomment-660115569. + // We could also include public members in private classes but to do + // that we'd need to ensure that there are no instances of either the + // enclosing class or any subclass of the enclosing class that are ever + // accessible outside this library. + if (parentIsPrivateExtension || + Identifier.isPrivateName(variable.name.lexeme)) { + _visit(variable); + } + } + } + super.visitFieldDeclaration(node); + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + var element = node.staticElement?.canonicalElement; + _visitIdentifierOrPropertyAccess(node, element); + super.visitPrefixedIdentifier(node); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + var element = node.propertyName.staticElement?.canonicalElement; + _visitIdentifierOrPropertyAccess(node, element); + super.visitPropertyAccess(node); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + var element = node.staticElement?.canonicalElement; + _visitIdentifierOrPropertyAccess(node, element); + super.visitSimpleIdentifier(node); + } + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + for (var variable in node.variables.variables) { + if (Identifier.isPrivateName(variable.name.lexeme)) { + _visit(variable); + } + } + super.visitTopLevelVariableDeclaration(node); + } + + void _checkAccess(Iterable units) { + var allNullableAccess = + units.expand((unit) => nullableAccess[unit] ?? const {}).toSet(); + for (var unit in units) { + for (var variable in lateables[unit] ?? const []) { + if (!allNullableAccess.contains(variable.declaredElement)) { + rule.reporter.reportError(AnalysisError.tmp( + source: unit.source, + offset: variable.offset, + length: variable.length, + errorCode: rule.lintCode)); + } + } + } + } + + void _visit(VariableDeclaration variable) { + if (variable.isLate) { + return; + } + if (variable.isSynthetic) { + return; + } + var declaredElement = variable.declaredElement; + if (declaredElement == null || + context.typeSystem.isNonNullable(declaredElement.type)) { + return; + } + lateables[currentUnit]?.add(variable); + } + + /// Checks whether [expression], which must be an [Identifier] or + /// [PropertyAccess], and its [canonicalElement], represent a nullable access. + void _visitIdentifierOrPropertyAccess( + Expression expression, Element? canonicalElement) { + assert(expression is Identifier || expression is PropertyAccess); + if (canonicalElement != null) { + var parent = expression.parent; + if (parent is Expression) { + parent = parent.unParenthesized; + } + if (expression is SimpleIdentifier && expression.inDeclarationContext()) { + // This is OK. + } else if (parent is PostfixExpression && + parent.operand == expression && + parent.operator.type == TokenType.BANG) { + // This is OK; non-null access. + } else { + nullableAccess[currentUnit]?.add(canonicalElement); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/use_named_constants.dart b/pkg/linter/lib/src/rules/use_named_constants.dart new file mode 100644 index 00000000000..a93461f0bed --- /dev/null +++ b/pkg/linter/lib/src/rules/use_named_constants.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const lintName = 'use_named_constants'; + +const _desc = r'Use predefined named constants.'; +const _details = r''' +Where possible, use already defined const values. + +**BAD:** +```dart +const Duration(seconds: 0); +``` + +**GOOD:** +```dart +Duration.zero; +``` + +'''; + +class UseNamedConstants extends LintRule { + static const LintCode code = LintCode('use_named_constants', + "Use the constant '{0}' rather than a constructor returning the same object.", + correctionMessage: "Try using '{0}'."); + + UseNamedConstants() + : super( + name: lintName, + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final LinterContext context; + _Visitor(this.rule, this.context); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.isConst) { + var type = node.staticType; + if (type is! InterfaceType) return; + var element = type.element; + if (element is ClassElement) { + var nodeField = + node.thisOrAncestorOfType()?.declaredElement; + + // avoid diagnostic for fields in the same class having the same value + // class A { + // const A(); + // static const a = A(); + // static const b = A(); + // } + if (nodeField?.enclosingElement == element) return; + + var library = (node.root as CompilationUnit).declaredElement?.library; + if (library == null) return; + var value = context.evaluateConstant(node).value; + for (var field + in element.fields.where((e) => e.isStatic && e.isConst)) { + if (field.isAccessibleIn(library) && + field.computeConstantValue() == value) { + rule.reportLint(node, arguments: ['${element.name}.${field.name}']); + return; + } + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/use_raw_strings.dart b/pkg/linter/lib/src/rules/use_raw_strings.dart new file mode 100644 index 00000000000..4abf6a7866d --- /dev/null +++ b/pkg/linter/lib/src/rules/use_raw_strings.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use raw string to avoid escapes.'; + +const _details = r''' +A raw string can be used to avoid escaping only backslashes and dollars. + +**BAD:** +```dart +var s = 'A string with only \\ and \$'; +``` + +**GOOD:** +```dart +var s = r'A string with only \ and $'; +``` + +'''; + +class UseRawStrings extends LintRule { + static const LintCode code = LintCode( + 'use_raw_strings', 'Use a raw string to avoid using escapes.', + correctionMessage: + 'Try making the string a raw string and removing the escapes.'); + + UseRawStrings() + : super( + name: 'use_raw_strings', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addSimpleStringLiteral(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (node.isRaw) return; + + var lexeme = node.literal.lexeme.substring( + node.contentsOffset - node.literal.offset, + node.contentsEnd - node.literal.offset); + var hasEscape = false; + for (var i = 0; i < lexeme.length - 1; i++) { + var current = lexeme[i]; + if (current == r'\') { + hasEscape = true; + i += 1; + current = lexeme[i]; + if (current != r'\' && current != r'$') { + return; + } + } + } + if (hasEscape) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_rethrow_when_possible.dart b/pkg/linter/lib/src/rules/use_rethrow_when_possible.dart new file mode 100644 index 00000000000..4e78a276b68 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_rethrow_when_possible.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use rethrow to rethrow a caught exception.'; + +const _details = r''' +**DO** use rethrow to rethrow a caught exception. + +As Dart provides rethrow as a feature, it should be used to improve terseness +and readability. + +**BAD:** +```dart +try { + somethingRisky(); +} catch(e) { + if (!canHandle(e)) throw e; + handle(e); +} +``` + +**GOOD:** +```dart +try { + somethingRisky(); +} catch(e) { + if (!canHandle(e)) rethrow; + handle(e); +} +``` + +'''; + +class UseRethrowWhenPossible extends LintRule { + static const LintCode code = LintCode('use_rethrow_when_possible', + "Use 'rethrow' to rethrow a caught exception.", + correctionMessage: "Try replacing the 'throw' with a 'rethrow'."); + + UseRethrowWhenPossible() + : super( + name: 'use_rethrow_when_possible', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addThrowExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitThrowExpression(ThrowExpression node) { + if (node.parent is Expression || node.parent is ArgumentList) return; + + var element = node.expression.canonicalElement; + if (element != null) { + var catchClause = node.thisOrAncestorOfType(); + var exceptionParameter = + catchClause?.exceptionParameter?.declaredElement?.canonicalElement; + if (element == exceptionParameter) { + rule.reportLint(node); + } + } + } +} diff --git a/pkg/linter/lib/src/rules/use_setters_to_change_properties.dart b/pkg/linter/lib/src/rules/use_setters_to_change_properties.dart new file mode 100644 index 00000000000..fdc504df7b5 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_setters_to_change_properties.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Use a setter for operations that conceptually change a property.'; + +const _details = r''' +**DO** use a setter for operations that conceptually change a property. + +**BAD:** +```dart +rectangle.setWidth(3); +button.setVisible(false); +``` + +**GOOD:** +```dart +rectangle.width = 3; +button.visible = false; +``` + +'''; + +// todo(pq): temporary; remove after renamed class is in the SDK +// ignore: non_constant_identifier_names +UseSettersToChangeAProperty() => UseSettersToChangeProperties(); + +class UseSettersToChangeProperties extends LintRule { + static const LintCode code = LintCode('use_setters_to_change_properties', + 'The method is used to change a property.', + correctionMessage: 'Try converting the method to a setter.'); + + UseSettersToChangeProperties() + : super( + name: 'use_setters_to_change_properties', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isSetter || + node.isGetter || + node.isOverride || + node.parameters?.parameters.length != 1 || + node.returnType?.type is! VoidType) { + return; + } + + void checkExpression(Expression expression) { + if (expression is AssignmentExpression && + expression.operator.type == TokenType.EQ) { + var leftOperand = expression.writeElement?.canonicalElement; + var rightOperand = expression.rightHandSide.canonicalElement; + var parameterElement = node.declaredElement?.parameters.first; + if (rightOperand == parameterElement && leftOperand is FieldElement) { + rule.reportLintForToken(node.name); + } + } + } + + var body = node.body; + if (body is BlockFunctionBody) { + if (body.block.statements.length == 1) { + var statement = body.block.statements.first; + if (statement is ExpressionStatement) { + checkExpression(statement.expression); + } + } + } else if (body is ExpressionFunctionBody) { + checkExpression(body.expression); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_string_buffers.dart b/pkg/linter/lib/src/rules/use_string_buffers.dart new file mode 100644 index 00000000000..99bf15fae50 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_string_buffers.dart @@ -0,0 +1,192 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = r'Use string buffers to compose strings.'; + +const _details = r''' +**DO** use string buffers to compose strings. + +In most cases, using a string buffer is preferred for composing strings due to +its improved performance. + +**BAD:** +```dart +String foo() { + final buffer = ''; + for (int i = 0; i < 10; i++) { + buffer += 'a'; // LINT + } + return buffer; +} +``` + +**GOOD:** +```dart +String foo() { + final buffer = StringBuffer(); + for (int i = 0; i < 10; i++) { + buffer.write('a'); + } + return buffer.toString(); +} +``` + +'''; + +bool _isEmptyInterpolationString(AstNode node) => + node is InterpolationString && node.value == ''; + +/// The motivation of this rule is performance stuffs, and the explanation is +/// that if we use N strings additions using the + operator the order of that +/// algorithm is O(~N^2) and that is because a String is not mutable, so in each +/// step it creates an auxiliary String that takes O(amount of chars) to be +/// computed, in otherwise using a StringBuffer the order is reduced to O(~N) +/// so the bad case is N times slower than the good case. +class UseStringBuffers extends LintRule { + static const LintCode code = LintCode('use_string_buffers', + "Use a string buffer rather than '+' to compose strings.", + correctionMessage: + 'Try writing the parts of a string to a string buffer.'); + + UseStringBuffers() + : super( + name: 'use_string_buffers', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addDoStatement(this, visitor); + registry.addForStatement(this, visitor); + registry.addWhileStatement(this, visitor); + } +} + +class _IdentifierIsPrefixVisitor extends SimpleAstVisitor { + final LintRule rule; + SimpleIdentifier identifier; + + _IdentifierIsPrefixVisitor(this.rule, this.identifier); + + @override + void visitBinaryExpression(BinaryExpression node) { + if (node.operator.type == TokenType.PLUS) { + node.leftOperand.accept(this); + } + } + + @override + void visitInterpolationExpression(InterpolationExpression node) { + node.expression.accept(this); + } + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + node.unParenthesized.accept(this); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (node.staticElement == identifier.staticElement) { + rule.reportLint(identifier); + } + } + + @override + void visitStringInterpolation(StringInterpolation node) { + if (node.elements.length >= 2 && + _isEmptyInterpolationString(node.elements.first)) { + node.elements[1].accept(this); + } + } +} + +class _UseStringBufferVisitor extends SimpleAstVisitor { + final LintRule rule; + final localElements = {}; + + _UseStringBufferVisitor(this.rule); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.operator.type != TokenType.PLUS_EQ && + node.operator.type != TokenType.EQ) return; + + var left = node.leftHandSide; + var writeType = node.writeType; + if (left is SimpleIdentifier && + writeType is InterfaceType && + writeType.isDartCoreString) { + if (node.operator.type == TokenType.PLUS_EQ && + !localElements.contains(node.writeElement?.canonicalElement)) { + rule.reportLint(node); + } + if (node.operator.type == TokenType.EQ) { + var visitor = _IdentifierIsPrefixVisitor(rule, left); + node.rightHandSide.accept(visitor); + } + } + } + + @override + void visitBlock(Block block) { + block.visitChildren(this); + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + node.expression.accept(this); + } + + @override + void visitParenthesizedExpression(ParenthesizedExpression node) { + node.unParenthesized.accept(this); + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + for (var variable in node.variables.variables) { + localElements.add(variable.declaredElement); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitDoStatement(DoStatement node) { + var visitor = _UseStringBufferVisitor(rule); + node.body.accept(visitor); + } + + @override + void visitForStatement(ForStatement node) { + var visitor = _UseStringBufferVisitor(rule); + node.body.accept(visitor); + } + + @override + void visitWhileStatement(WhileStatement node) { + var visitor = _UseStringBufferVisitor(rule); + node.body.accept(visitor); + } +} diff --git a/pkg/linter/lib/src/rules/use_string_in_part_of_directives.dart b/pkg/linter/lib/src/rules/use_string_in_part_of_directives.dart new file mode 100644 index 00000000000..b635311651d --- /dev/null +++ b/pkg/linter/lib/src/rules/use_string_in_part_of_directives.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use string in part of directives.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/usage#do-use-strings-in-part-of-directives): + +**DO** use strings in `part of` directives. + +**BAD:** + +```dart +part of my_library; +``` + +**GOOD:** + +```dart +part of '../../my_library.dart'; +``` + +'''; + +class UseStringInPartOfDirectives extends LintRule { + static const LintCode code = LintCode('use_string_in_part_of_directives', + 'The part-of directive uses a library name.', + correctionMessage: + 'Try converting the directive to use the URI of the library.'); + + UseStringInPartOfDirectives() + : super( + name: 'use_string_in_part_of_directives', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, + LinterContext context, + ) { + var visitor = _Visitor(this); + registry.addPartOfDirective(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitPartOfDirective(PartOfDirective node) { + if (node.libraryName != null) { + rule.reportLint(node); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_super_parameters.dart b/pkg/linter/lib/src/rules/use_super_parameters.dart new file mode 100644 index 00000000000..749457a589d --- /dev/null +++ b/pkg/linter/lib/src/rules/use_super_parameters.dart @@ -0,0 +1,276 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/src/utilities/extensions/string.dart'; // ignore: implementation_imports + +import '../analyzer.dart'; + +const _desc = r'Use super-initializer parameters where possible.'; + +const _details = r''' +"Forwarding constructor"s, that do nothing except forward parameters to their +superclass constructors should take advantage of super-initializer parameters +rather than repeating the names of parameters when passing them to the +superclass constructors. This makes the code more concise and easier to read +and maintain. + +**DO** use super-initializer parameters where possible. + +**BAD:** +```dart +class A { + A({int? x, int? y}); +} +class B extends A { + B({int? x, int? y}) : super(x: x, y: y); +} +``` + +**GOOD:** +```dart +class A { + A({int? x, int? y}); +} +class B extends A { + B({super.x, super.y}); +} +``` +'''; + +/// Return a set containing the elements of all of the parameters that are +/// referenced in the body of the [constructor]. +Set _referencedParameters( + ConstructorDeclaration constructor) { + var collector = _ReferencedParameterCollector(); + constructor.body.accept(collector); + return collector.foundParameters; +} + +class UseSuperParameters extends LintRule { + static const LintCode singleParam = + LintCode('use_super_parameters', "Convert '{0}' to a super parameter."); + static const LintCode multipleParams = + LintCode('use_super_parameters', 'Convert {0} to super parameters.'); + + UseSuperParameters() + : super( + name: 'use_super_parameters', + description: _desc, + details: _details, + state: State.experimental(), + group: Group.style); + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + if (!context.isEnabled(Feature.super_parameters)) return; + + var visitor = _Visitor(this, context); + registry.addConstructorDeclaration(this, visitor); + } +} + +class _ReferencedParameterCollector extends RecursiveAstVisitor { + final Set foundParameters = {}; + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + var element = node.staticElement; + if (element is ParameterElement) { + foundParameters.add(element); + } + } +} + +class _Visitor extends SimpleAstVisitor { + final LinterContext context; + final LintRule rule; + + _Visitor(this.rule, this.context); + + void check( + ConstructorDeclaration node, + SuperConstructorInvocation superInvocation, + FormalParameterList parameters) { + var constructorElement = superInvocation.staticElement; + if (constructorElement == null) return; + + // todo(pq): consolidate logic shared w/ server + // (https://github.com/dart-lang/linter/issues/3263) + + var referencedParameters = _referencedParameters(node); + + var identifiers = _checkForConvertiblePositionalParams( + constructorElement, superInvocation, parameters, referencedParameters); + + // Bail if there are positional params that can't be converted. + if (identifiers == null) return; + + for (var parameter in parameters.parameters) { + var parameterElement = parameter.declaredElement; + if (parameterElement == null) continue; + if (parameterElement is FieldFormalParameterElement) continue; + if (parameterElement.isNamed && + !referencedParameters.contains(parameterElement)) { + if (_checkNamedParameter( + parameter, parameterElement, constructorElement, superInvocation)) { + var identifier = parameter.name?.lexeme; + if (identifier != null) { + identifiers.add(identifier); + } + } + } + } + + _reportLint(node, identifiers); + } + + @override + visitConstructorDeclaration(ConstructorDeclaration node) { + for (var initializer in node.initializers.reversed) { + if (initializer is SuperConstructorInvocation) { + check(node, initializer, node.parameters); + return; + } + } + } + + /// Check if all super positional parameters can be converted to use super- + /// initializers. Return a list of convertible named parameters or `null` if + /// there are parameters that can't be converted since this will short-circuit + /// the lint. + List? _checkForConvertiblePositionalParams( + ConstructorElement constructorElement, + SuperConstructorInvocation superInvocation, + FormalParameterList parameters, + Set referencedParameters) { + var positionalSuperArgs = []; + for (var arg in superInvocation.argumentList.arguments) { + if (arg is SimpleIdentifier) { + positionalSuperArgs.add(arg); + } else if (arg is! NamedExpression) { + return null; + } + } + + if (positionalSuperArgs.isEmpty) return []; + + var constructorParams = parameters.parameters; + var convertibleConstructorParams = []; + var matchedConstructorParamIndex = 0; + + var seenSuperParams = {}; + + // For each super arg, ensure there is a constructor param (in the right + // order). + for (var i = 0; i < positionalSuperArgs.length; ++i) { + var superArg = positionalSuperArgs[i]; + var superParam = superArg.staticElement; + if (superParam is! ParameterElement) return null; + if (superParam.isNamed) return null; + + // Check for the case where a super param is used more than once. + if (!seenSuperParams.add(superParam)) return null; + + bool match = false; + for (var i = 0; i < constructorParams.length && !match; ++i) { + var constructorParam = constructorParams[i]; + if (constructorParam is FieldFormalParameter) return null; + if (constructorParam is SuperFormalParameter) return null; + var constructorElement = constructorParam.declaredElement; + if (constructorElement == null) continue; + if (referencedParameters.contains(constructorElement)) return null; + if (constructorElement == superParam) { + // Compare the types. + var superType = superParam.type; + var argType = constructorElement.type; + if (!context.typeSystem.isSubtypeOf(argType, superType)) { + return null; + } + + match = true; + var identifier = constructorParam.name?.lexeme; + if (identifier != null) { + convertibleConstructorParams.add(identifier); + } + // Ensure we're not out of order. + if (i < matchedConstructorParamIndex) return null; + matchedConstructorParamIndex = i; + } + } + } + + return convertibleConstructorParams; + } + + /// Return `true` if the named [parameter] can be converted into a super + /// initializing formal parameter. + bool _checkNamedParameter( + FormalParameter parameter, + ParameterElement parameterElement, + ConstructorElement superConstructor, + SuperConstructorInvocation superInvocation) { + var superParameter = + _correspondingNamedParameter(superConstructor, parameterElement); + if (superParameter == null) return false; + + bool matchingArgument = false; + var arguments = superInvocation.argumentList.arguments; + for (var argument in arguments) { + if (argument is NamedExpression && + argument.name.label.name == parameterElement.name) { + var expression = argument.expression; + if (expression is SimpleIdentifier && + expression.staticElement == parameterElement) { + matchingArgument = true; + break; + } + } + } + if (!matchingArgument) { + // If the parameter isn't being passed to the super constructor, then + // don't lint. + return false; + } + + // Compare the types. + var superType = superParameter.type; + var thisType = parameterElement.type; + if (!context.typeSystem.isAssignableTo(superType, thisType)) { + // If the type of the parameter can't be assigned to the super parameter, + // then don't lint. + return false; + } + + return true; + } + + ParameterElement? _correspondingNamedParameter( + ConstructorElement superConstructor, ParameterElement thisParameter) { + for (var superParameter in superConstructor.parameters) { + if (superParameter.isNamed && superParameter.name == thisParameter.name) { + return superParameter; + } + } + return null; + } + + void _reportLint(ConstructorDeclaration node, List identifiers) { + if (identifiers.isEmpty) return; + var target = node.name ?? node.returnType; + if (identifiers.length > 1) { + var msg = identifiers.quotedAndCommaSeparatedWithAnd; + rule.reportLintForOffset(target.offset, target.length, + errorCode: UseSuperParameters.multipleParams, arguments: [msg]); + } else { + rule.reportLintForOffset(target.offset, target.length, + errorCode: UseSuperParameters.singleParam, + arguments: [identifiers.first]); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_test_throws_matchers.dart b/pkg/linter/lib/src/rules/use_test_throws_matchers.dart new file mode 100644 index 00000000000..47ba821ef4e --- /dev/null +++ b/pkg/linter/lib/src/rules/use_test_throws_matchers.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use throwsA matcher instead of fail().'; + +const _details = r''' +Use the `throwsA` matcher instead of try-catch with `fail()`. + +**BAD:** + +```dart +// sync code +try { + someSyncFunctionThatThrows(); + fail('expected Error'); +} on Error catch (error) { + expect(error.message, contains('some message')); +} + +// async code +try { + await someAsyncFunctionThatThrows(); + fail('expected Error'); +} on Error catch (error) { + expect(error.message, contains('some message')); +} +``` + +**GOOD:** +```dart +// sync code +expect( + () => someSyncFunctionThatThrows(), + throwsA(isA().having((Error error) => error.message, 'message', contains('some message'))), +); + +// async code +await expectLater( + () => someAsyncFunctionThatThrows(), + throwsA(isA().having((Error error) => error.message, 'message', contains('some message'))), +); +``` + +'''; + +class UseTestThrowsMatchers extends LintRule { + static const LintCode code = LintCode( + 'use_test_throws_matchers', + "Use the 'throwsA' matcher instead of using 'fail' when there is no " + 'exception thrown.', + correctionMessage: + "Try removing the try-catch and using 'throwsA' to expect an exception."); + + UseTestThrowsMatchers() + : super( + name: 'use_test_throws_matchers', + description: _desc, + details: _details, + group: Group.style, + ); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addTryStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + bool isTestInvocation(Statement statement, String functionName) { + if (statement is! ExpressionStatement) return false; + var expression = statement.expression; + if (expression is! MethodInvocation) return false; + var element = expression.methodName.staticElement; + return element is FunctionElement && + element.source.uri == + Uri.parse('package:test_api/src/frontend/expect.dart') && + element.name == functionName; + } + + @override + void visitTryStatement(TryStatement node) { + if (node.catchClauses.length != 1 || node.body.statements.isEmpty) return; + + var lastBodyStatement = node.body.statements.last; + + if (isTestInvocation(lastBodyStatement, 'fail') && + node.finallyBlock == null) { + rule.reportLint(lastBodyStatement); + } + } +} diff --git a/pkg/linter/lib/src/rules/use_to_and_as_if_applicable.dart b/pkg/linter/lib/src/rules/use_to_and_as_if_applicable.dart new file mode 100644 index 00000000000..84b14012346 --- /dev/null +++ b/pkg/linter/lib/src/rules/use_to_and_as_if_applicable.dart @@ -0,0 +1,121 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +const _desc = + r'Start the name of the method with to/_to or as/_as if applicable.'; + +const _details = r''' +From [Effective Dart](https://dart.dev/effective-dart/design#prefer-naming-a-method-to___-if-it-copies-the-objects-state-to-a-new-object): + +**PREFER** naming a method `to___()` if it copies the object's state to a new +object. + +**PREFER** naming a method `as___()` if it returns a different representation +backed by the original object. + +**BAD:** +```dart +class Bar { + Foo myMethod() { + return Foo.from(this); + } +} +``` + +**GOOD:** +```dart +class Bar { + Foo toFoo() { + return Foo.from(this); + } +} +``` + +**GOOD:** +```dart +class Bar { + Foo asFoo() { + return Foo.from(this); + } +} +``` + +'''; + +bool _beginsWithAsOrTo(String name) { + var regExp = RegExp(r'(to|as|_to|_as)[A-Z]'); + return regExp.matchAsPrefix(name) != null; +} + +bool _isVoid(TypeAnnotation? returnType) => + returnType is NamedType && returnType.type is VoidType; + +class UseToAndAsIfApplicable extends LintRule { + static const LintCode code = LintCode('use_to_and_as_if_applicable', + "Start the name of the method with 'to' or 'as'.", + correctionMessage: "Try renaming the method to use either 'to' or 'as'."); + + UseToAndAsIfApplicable() + : super( + name: 'use_to_and_as_if_applicable', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addMethodDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + var nodeParameters = node.parameters; + if (!node.isGetter && + nodeParameters != null && + nodeParameters.parameters.isEmpty && + !_isVoid(node.returnType) && + !_beginsWithAsOrTo(node.name.lexeme) && + !node.hasInheritedMethod && + _checkBody(node.body)) { + rule.reportLintForToken(node.name); + } + } + + bool _checkBody(FunctionBody body) { + if (body is ExpressionFunctionBody) { + return _checkExpression(body.expression); + } else if (body is BlockFunctionBody && body.block.statements.length == 1) { + var statement = body.block.statements.first; + if (statement is ReturnStatement) { + return _checkExpression(statement.expression); + } + } + return false; + } + + bool _checkExpression(Expression? rawExpression) { + var expression = rawExpression?.unParenthesized; + return expression is InstanceCreationExpression && + expression.argumentList.arguments.length == 1 && + expression.argumentList.arguments.first is ThisExpression; + } +} diff --git a/pkg/linter/lib/src/rules/valid_regexps.dart b/pkg/linter/lib/src/rules/valid_regexps.dart new file mode 100644 index 00000000000..a09b0a067e1 --- /dev/null +++ b/pkg/linter/lib/src/rules/valid_regexps.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +import '../analyzer.dart'; + +const _desc = r'Use valid regular expression syntax.'; + +const _details = r''' +**DO** use valid regular expression syntax when creating regular expression +instances. + +Regular expressions created with invalid syntax will throw a `FormatException` +at runtime so should be avoided. + +**BAD:** +```dart +print(RegExp(r'(').hasMatch('foo()')); +``` + +**GOOD:** +```dart +print(RegExp(r'\(').hasMatch('foo()')); +``` + +'''; + +class ValidRegexps extends LintRule { + static const LintCode code = LintCode( + 'valid_regexps', 'Invalid regular expression syntax.', + correctionMessage: 'Try correcting the regular expression.'); + + ValidRegexps() + : super( + name: 'valid_regexps', + description: _desc, + details: _details, + group: Group.errors); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + _Visitor(this.rule); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var element = node.constructorName.staticElement?.enclosingElement; + if (element?.name == 'RegExp' && element?.library.name == 'dart.core') { + var args = node.argumentList.arguments; + if (args.isEmpty) { + return; + } + + bool isTrue(Expression e) => e is BooleanLiteral && e.value; + + var unicode = args.any((arg) => + arg is NamedExpression && + arg.name.label.name == 'unicode' && + isTrue(arg.expression)); + + var sourceExpression = args.first; + if (sourceExpression is StringLiteral) { + var source = sourceExpression.stringValue; + if (source != null) { + try { + RegExp(source, unicode: unicode); + } on FormatException { + rule.reportLint(sourceExpression); + } + } + } + } + } +} diff --git a/pkg/linter/lib/src/rules/void_checks.dart b/pkg/linter/lib/src/rules/void_checks.dart new file mode 100644 index 00000000000..69bf1a60dbc --- /dev/null +++ b/pkg/linter/lib/src/rules/void_checks.dart @@ -0,0 +1,187 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../analyzer.dart'; + +const _desc = r"Don't assign to void."; + +const _details = r''' +**DON'T** assign to void. + +**BAD:** +```dart +class A { + T value; + void test(T arg) { } +} + +void main() { + A a = A(); + a.value = 1; // LINT + a.test(1); // LINT +} +``` +'''; + +class VoidChecks extends LintRule { + static const LintCode code = LintCode( + 'void_checks', "Assignment to a variable of type 'void'.", + correctionMessage: + 'Try removing the assignment or changing the type of the variable.'); + + VoidChecks() + : super( + name: 'void_checks', + description: _desc, + details: _details, + group: Group.style); + + @override + LintCode get lintCode => code; + + @override + void registerNodeProcessors( + NodeLintRegistry registry, LinterContext context) { + var visitor = _Visitor(this, context); + registry.addAssignedVariablePattern(this, visitor); + registry.addAssignmentExpression(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); + registry.addReturnStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + + final TypeSystem typeSystem; + + _Visitor(this.rule, LinterContext context) : typeSystem = context.typeSystem; + + bool isTypeAcceptableWhenExpectingFutureOrVoid(DartType type) { + if (type is DynamicType) return true; + if (isTypeAcceptableWhenExpectingVoid(type)) return true; + if (type.isDartAsyncFutureOr || + type.isDartAsyncFuture && + type is InterfaceType && + isTypeAcceptableWhenExpectingFutureOrVoid( + type.typeArguments.first)) { + return true; + } + + return false; + } + + bool isTypeAcceptableWhenExpectingVoid(DartType type) { + if (type is VoidType) return true; + if (type.isDartCoreNull) return true; + if (type is NeverType) return true; + if (type.isDartAsyncFuture && + type is InterfaceType && + isTypeAcceptableWhenExpectingVoid(type.typeArguments.first)) { + return true; + } + return false; + } + + @override + void visitAssignedVariablePattern(AssignedVariablePattern node) { + var valueType = node.matchedValueType; + var element = node.element; + if (element is! VariableElement) return; + _check(element.type, valueType, node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + var type = node.writeType; + _check(type, node.rightHandSide.staticType, node, + checkedNode: node.rightHandSide); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + var args = node.argumentList.arguments; + var parameters = node.constructorName.staticElement?.parameters; + if (parameters != null) { + _checkArgs(args, parameters); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + var type = node.staticInvokeType; + if (type is FunctionType) { + var args = node.argumentList.arguments; + var parameters = type.parameters; + _checkArgs(args, parameters); + } + } + + @override + void visitReturnStatement(ReturnStatement node) { + var parent = node.thisOrAncestorMatching((e) => + e is FunctionExpression || + e is MethodDeclaration || + e is FunctionDeclaration); + if (parent is FunctionExpression) { + var type = parent.staticType; + if (type is FunctionType) { + _check(type.returnType, node.expression?.staticType, node, + checkedNode: node.expression); + } + } else if (parent is MethodDeclaration) { + _check( + parent.declaredElement?.returnType, node.expression?.staticType, node, + checkedNode: node.expression); + } else if (parent is FunctionDeclaration) { + _check( + parent.declaredElement?.returnType, node.expression?.staticType, node, + checkedNode: node.expression); + } + } + + void _check(DartType? expectedType, DartType? type, AstNode node, + {AstNode? checkedNode}) { + checkedNode ??= node; + if (expectedType == null || type == null) { + return; + } + if (expectedType is VoidType && + type is! DynamicType && + node is ReturnStatement) { + return; + } + if (expectedType is VoidType && !isTypeAcceptableWhenExpectingVoid(type)) { + rule.reportLint(node); + } else if (expectedType.isDartAsyncFutureOr && + (expectedType as InterfaceType).typeArguments.first is VoidType && + !isTypeAcceptableWhenExpectingFutureOrVoid(type)) { + rule.reportLint(node); + } else if (checkedNode is FunctionExpression && + checkedNode.body is! ExpressionFunctionBody && + expectedType is FunctionType && + type is FunctionType) { + _check(expectedType.returnType, type.returnType, node, + checkedNode: checkedNode); + } + } + + void _checkArgs( + NodeList args, List parameters) { + for (var arg in args) { + var parameterElement = arg.staticParameterElement; + if (parameterElement != null) { + var type = parameterElement.type; + var expression = arg is NamedExpression ? arg.expression : arg; + _check(type, expression.staticType, expression); + } + } + } +} diff --git a/pkg/linter/lib/src/test_utilities/annotation.dart b/pkg/linter/lib/src/test_utilities/annotation.dart new file mode 100644 index 00000000000..75ea1506ef6 --- /dev/null +++ b/pkg/linter/lib/src/test_utilities/annotation.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/line_info.dart'; + +Annotation? extractAnnotation(int lineNumber, String line) { + var regexp = + RegExp(r'(//|#) ?LINT( \[([\-+]\d+)?(,?(\d+):(\d+))?\])?( (.*))?$'); + var match = regexp.firstMatch(line); + if (match == null) return null; + + // ignore lints on commented out lines + var index = match.start; + var comment = match[1]!; + if (line.indexOf(comment) != index) return null; + + var relativeLine = match[3].toInt() ?? 0; + var column = match[5].toInt(); + var length = match[6].toInt(); + var message = match[8].toNullIfBlank(); + return Annotation.forLint(message, column, length) + ..lineNumber = lineNumber + relativeLine; +} + +/// Information about a 'LINT' annotation/comment. +class Annotation implements Comparable { + final int? column; + final int? length; + final String? message; + final ErrorType type; + int? lineNumber; + + Annotation(this.message, this.type, this.lineNumber, + {this.column, this.length}); + + Annotation.forError(AnalysisError error, LineInfo lineInfo) + : this(error.message, error.errorCode.type, + lineInfo.getLocation(error.offset).lineNumber, + column: lineInfo.getLocation(error.offset).columnNumber, + length: error.length); + + Annotation.forLint([String? message, int? column, int? length]) + : this(message, ErrorType.LINT, null, column: column, length: length); + + @override + int compareTo(Annotation other) { + if (lineNumber != other.lineNumber) { + return lineNumber! - other.lineNumber!; + } else if (column != other.column) { + return column! - other.column!; + } + return message!.compareTo(other.message!); + } + + @override + String toString() => + '[$type]: "$message" (line: $lineNumber) - [$column:$length]'; +} + +extension on String? { + int? toInt() => this == null ? null : int.parse(this!); + String? toNullIfBlank() => this == null || this!.trim().isEmpty ? null : this; +} diff --git a/pkg/linter/lib/src/test_utilities/test_resource_provider.dart b/pkg/linter/lib/src/test_utilities/test_resource_provider.dart new file mode 100644 index 00000000000..696c1ee5c83 --- /dev/null +++ b/pkg/linter/lib/src/test_utilities/test_resource_provider.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/file_system/file_system.dart' as file_system; +import 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/test_utilities/mock_sdk.dart'; +import 'package:path/path.dart' as path; + +import '../analyzer.dart'; + +/// Builds the [DartLinter] with appropriate mock SDK, resource providers, and +/// package config path. +DartLinter buildDriver(LintRule rule, File file, {String? analysisOptions}) { + var memoryResourceProvider = MemoryResourceProvider( + context: PhysicalResourceProvider.INSTANCE.pathContext); + var resourceProvider = TestResourceProvider(memoryResourceProvider); + + var sdkRoot = memoryResourceProvider.getFolder( + memoryResourceProvider.convertPath('/sdk'), + ); + createMockSdk( + resourceProvider: memoryResourceProvider, + root: sdkRoot, + ); + + var options = LinterOptions([rule], analysisOptions) + ..dartSdkPath = sdkRoot.path + ..resourceProvider = resourceProvider; + + return DartLinter(options); +} + +/// A resource provider that accesses entities in a MemoryResourceProvider, +/// falling back to the PhysicalResourceProvider when they don't exist. +class TestResourceProvider extends file_system.ResourceProvider { + static final PhysicalResourceProvider physicalResourceProvider = + PhysicalResourceProvider.INSTANCE; + + final MemoryResourceProvider memoryResourceProvider; + + TestResourceProvider(this.memoryResourceProvider); + + @override + path.Context get pathContext => physicalResourceProvider.pathContext; + + @override + file_system.File getFile(String path) { + var file = memoryResourceProvider.getFile(path); + return file.exists ? file : physicalResourceProvider.getFile(path); + } + + @override + file_system.Folder getFolder(String path) { + var folder = memoryResourceProvider.getFolder(path); + return folder.exists ? folder : physicalResourceProvider.getFolder(path); + } + + @override + file_system.Resource getResource(String path) { + var resource = memoryResourceProvider.getResource(path); + return resource.exists + ? resource + : physicalResourceProvider.getResource(path); + } + + @override + file_system.Folder? getStateLocation(String pluginId) => + physicalResourceProvider.getStateLocation(pluginId); +} diff --git a/pkg/linter/lib/src/util/ascii_utils.dart b/pkg/linter/lib/src/util/ascii_utils.dart new file mode 100644 index 00000000000..b58be9eb32e --- /dev/null +++ b/pkg/linter/lib/src/util/ascii_utils.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// String utilities that use underlying ASCII codes for improved performance. +/// Ultimately we should consider carefully when we use RegExps where a simple +/// loop would do (and would do so far more performantly). +/// See: https://github.com/dart-lang/linter/issues/1828 +library ascii_utils; + +import 'charcodes.dart'; + +/// Return `true` if the given [character] is the ASCII '.' character. +bool isDot(int character) => character == $dot; + +/// Return `true` if the given [character] is a lowercase ASCII character. +bool isLowerCase(int character) => character >= $a && character <= $z; + +/// Return `true` if the given [character] an ASCII number character. +bool isNumber(int character) => character >= 48 && character <= 57; + +/// Return `true` if the given [character] is the ASCII '_' character. +bool isUnderScore(int character) => character == $_; + +/// Check if the given [name] is a valid Dart filename. +/// +/// Files with a strict `.dart` extension are required to use: +/// * `lower_snake_case` and are +/// * limited to valid Dart identifiers +/// +/// (Files without a strict `.dart` extension are considered valid.) +bool isValidDartFileName(String name) { + if (name.length < 6 || !name.endsWith('.dart')) { + return true; + } + + var length = name.length - 5; + for (var i = 1; i < length - 1; ++i) { + var character = name.codeUnitAt(i); + // Indicates a prefixed suffix (like `.g.dart`) which is considered a + // non-strict Dart filename. + if (isDot(character)) { + return true; + } + } + + for (var i = 0; i < length; ++i) { + var character = name.codeUnitAt(i); + if (!isLowerCase(character) && !isUnderScore(character)) { + if (isNumber(character)) { + if (i == 0) { + return false; + } + continue; + } + return false; + } + } + return true; +} + +extension StringExtensions on String { + /// Returns `true` if `this` has a leading `_`. + bool get hasLeadingUnderscore => startsWith('_'); + + /// Returns whether `this` is just underscores. + bool get isJustUnderscores { + if (isEmpty) { + return false; + } + switch (length) { + case 1: + return this == '_'; + case 2: + return this == '__'; + default: + for (var i = 0; i < length; i++) { + if (!isUnderScore(codeUnitAt(i))) { + return false; + } + } + return true; + } + } +} diff --git a/pkg/linter/lib/src/util/charcodes.dart b/pkg/linter/lib/src/util/charcodes.dart new file mode 100644 index 00000000000..3966d239ab1 --- /dev/null +++ b/pkg/linter/lib/src/util/charcodes.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Character `.`. +const int $dot = 0x2e; + +/// Character `0`. +const int $0 = 0x30; + +/// Character `9`. +const int $9 = 0x39; + +/// Character `\`. +const int $backslash = 0x5c; + +/// Character `_`. +const int $_ = 0x5f; + +/// Character `a`. +const int $a = 0x61; + +/// Character `z`. +const int $z = 0x7a; + +/// Character `|`. +const int $pipe = 0x7c; diff --git a/pkg/linter/lib/src/util/condition_scope_visitor.dart b/pkg/linter/lib/src/util/condition_scope_visitor.dart new file mode 100644 index 00000000000..f8f59854015 --- /dev/null +++ b/pkg/linter/lib/src/util/condition_scope_visitor.dart @@ -0,0 +1,443 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +import '../analyzer.dart'; +import '../extensions.dart'; + +Element? _getLeftElement(AssignmentExpression assignment) => + assignment.writeElement?.canonicalElement; + +List _splitConjunctions(Expression? rawExpression) { + var expression = rawExpression?.unParenthesized; + if (expression is BinaryExpression && + expression.operator.type == TokenType.AMPERSAND_AMPERSAND) { + return _splitConjunctions(expression.leftOperand) + ..addAll(_splitConjunctions(expression.rightOperand)); + } + return [expression]; +} + +class BreakScope { + var environment = []; + + void add(BreakStatement element) { + if (element.target != null) { + environment.add(element); + } + } + + void deleteBreaksWithTarget(AstNode node) { + environment = environment.where((e) => e.target != node).toList(); + } + + bool hasBreak(AstNode node) => environment.any((e) => e.target == node); +} + +class ConditionScope { + final environment = []; + final ConditionScope? outer; + + ConditionScope(this.outer); + + void add(ExpressionBox e) { + environment.add(e); + } + + void addAll(Iterable expressions) { + environment.addAll(expressions); + } + + Iterable getExpressions(Iterable elements, + {bool? value}) { + var expressions = []; + _recursiveGetExpressions(expressions, elements, value); + return expressions; + } + + Iterable getUndefinedExpressions() => + environment.whereType<_UndefinedExpression>(); + + void _recursiveGetExpressions( + List expressions, Iterable elements, bool? value) { + for (var element in environment.reversed) { + if (element.haveToStop(elements)) { + return; + } + if (element is _ConditionExpression && element.value == value) { + var expression = element.expression; + if (expression != null) { + expressions.add(expression); + } + } + } + outer?._recursiveGetExpressions(expressions, elements, value); + } +} + +/// An AST visitor that keeps the conditions that are currently evaluated +/// to true or false. +/// +/// Subclasses that override a visit method must either invoke the overridden +/// visit method or must explicitly ask the visited node to visit its children. +/// Failure to do so will cause the children of the visited node to not be +/// visited. +/// +/// Subclasses that override a visit method must either invoke the overridden +/// visit method and keep the scopes behavior consistent to its changes. +/// +/// When add a new local scope: +/// - Visiting a non-empty function body: [BlockFunctionBody] [ExpressionFunctionBody]. +/// - Visiting a flow-control statement: [IfStatement] ElseStatement. +/// - Visiting loop statements: [DoStatement] [WhileStatement] [ForStatement]. +/// +/// When call the abstract method visitCondition(node.condition): +/// - After visiting a conditional statements: [IfStatement] [DoStatement] [WhileStatement]. +/// +/// When undefine an element: +/// - Visiting reassignments of variables: [AssignmentExpression] [PrefixExpression] [PostfixExpression]. +/// +/// When undefine all elements: +/// - Visiting a non-empty function body: [BlockFunctionBody] [ExpressionFunctionBody]. +/// - Visiting clauses that generates dead_code: [ReturnStatement] [ThrowExpression] [RethrowExpression]. +/// - Visiting if/else with exit clauses inside in both cases (also generates dead code). +/// +/// When propagate undefined elements: +/// - After visiting a flow-control statement: [IfStatement] ElseStatement. +/// - After visiting loop statements: [DoStatement] [WhileStatement] [ForStatement]. +/// +/// When add a Condition as true condition: +/// - Inside an if body and after an else body with exit clause. +/// - Inside pre evaluated conditional loops: [ForStatement] [WhileStatement] +/// +/// When add a Condition as false condition: +/// - Inside an else body and after a then body with exit clause. +/// - Outside pre evaluated conditional loops without breaks: [ForStatement] [WhileStatement] +/// +/// When add a BreakStatement in the breakScope. +/// - When visiting a BreakStatement. +/// +/// When remove a BreakStatement in the breakScope. +/// - After visiting the target of the BreakStatement. +/// +/// Clients may extend this class. +abstract class ConditionScopeVisitor extends RecursiveAstVisitor { + ConditionScope? outerScope; + final breakScope = BreakScope(); + + /// todo (pq): here and w/ getTrueExpressions, consider an empty iterable + Iterable? getFalseExpressions(Iterable elements) => + _getExpressions(elements, value: false); + + Iterable? getTrueExpressions(Iterable elements) => + _getExpressions(elements); + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _addElementToEnvironment( + _UndefinedExpression.forElement(_getLeftElement(node))); + node.visitChildren(this); + } + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + _addScope(); + _addElementToEnvironment(_UndefinedAllExpression()); + node.visitChildren(this); + _removeLastScope(); + } + + @override + void visitBreakStatement(BreakStatement node) { + breakScope.add(node); + node.visitChildren(this); + } + + void visitCondition(Expression? node); + + @override + void visitDoStatement(DoStatement node) { + _addScope(); + visitCondition(node.condition); + node.visitChildren(this); + _propagateUndefinedExpressions(_removeLastScope()); + // If a do statement do not have breaks inside, that means the condition + // after the loop is false. + if (!breakScope.hasBreak(node)) { + _addFalseCondition(node.condition); + } + breakScope.deleteBreaksWithTarget(node); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _addScope(); + _addElementToEnvironment(_UndefinedAllExpression()); + node.visitChildren(this); + _removeLastScope(); + } + + @override + void visitForStatement(ForStatement node) { + _addScope(); + var loopParts = node.forLoopParts; + if (loopParts is ForParts) { + _addTrueCondition(loopParts.condition); + + if (loopParts is ForPartsWithDeclarations) { + loopParts.variables.accept(this); + } else if (loopParts is ForPartsWithExpression) { + loopParts.initialization?.accept(this); + } + + visitCondition(loopParts.condition); + loopParts.condition?.accept(this); + _addTrueCondition(loopParts.condition); + loopParts.updaters.accept(this); + node.body.accept(this); + _propagateUndefinedExpressions(_removeLastScope()); + if (_isRelevantOutsideOfForStatement(node)) { + _addFalseCondition(loopParts.condition); + } + breakScope.deleteBreaksWithTarget(node); + } else if (loopParts is ForEachParts) { + node.visitChildren(this); + _propagateUndefinedExpressions(_removeLastScope()); + } else { + throw StateError('unsupported loop parts type'); + } + } + + @override + void visitIfStatement(IfStatement node) { + var elseScope = _visitElseStatement(node.elseStatement, node.expression); + _visitIfStatement(node); + if (elseScope != null) { + _propagateUndefinedExpressions(elseScope); + } + var addFalseCondition = _isLastStatementAnExitStatement(node.thenStatement); + var addTrueCondition = _isLastStatementAnExitStatement(node.elseStatement); + // If addTrueCondition and addFalseCondition are true at the same time, + // then the rest of the block is dead code. + if (addTrueCondition && addFalseCondition) { + _addElementToEnvironment(_UndefinedAllExpression()); + return; + } + if (addFalseCondition) { + _addFalseCondition(node.expression); + } + if (addTrueCondition) { + _addTrueCondition(node.expression); + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + var operand = node.operand; + if (operand is SimpleIdentifier) { + _addElementToEnvironment( + _UndefinedExpression.forElement(operand.staticElement)); + } + node.visitChildren(this); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + var operand = node.operand; + if (operand is SimpleIdentifier) { + _addElementToEnvironment( + _UndefinedExpression.forElement(operand.staticElement)); + } + node.visitChildren(this); + } + + @override + void visitRethrowExpression(RethrowExpression node) { + node.visitChildren(this); + _addElementToEnvironment(_UndefinedAllExpression()); + } + + @override + void visitReturnStatement(ReturnStatement node) { + node.visitChildren(this); + _addElementToEnvironment(_UndefinedAllExpression()); + } + + @override + void visitThrowExpression(ThrowExpression node) { + node.visitChildren(this); + _addElementToEnvironment(_UndefinedAllExpression()); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + _addElementToEnvironment( + _UndefinedExpression.forElement(node.declaredElement)); + node.visitChildren(this); + } + + @override + void visitWhileStatement(WhileStatement node) { + _addScope(); + visitCondition(node.condition); + node.condition.accept(this); + _addTrueCondition(node.condition); + node.body.accept(this); + _propagateUndefinedExpressions(_removeLastScope()); + // If a while statement do not have breaks inside, that means the condition + // after the loop is false. + if (!breakScope.hasBreak(node)) { + _addFalseCondition(node.condition); + } + breakScope.deleteBreaksWithTarget(node); + } + + void _addElementToEnvironment(ExpressionBox? e) { + if (e != null) { + outerScope?.add(e); + } + } + + void _addFalseCondition(Expression? expression) { + _addElementToEnvironment(_ConditionExpression(expression, value: false)); + } + + void _addScope() { + outerScope = ConditionScope(outerScope); + } + + void _addTrueCondition(Expression? expression) { + _splitConjunctions(expression).forEach((e) { + _addElementToEnvironment(_ConditionExpression(e)); + }); + } + + Iterable? _getExpressions(Iterable elements, + {bool value = true}) => + outerScope?.getExpressions(elements, value: value); + + bool _isLastStatementAnExitStatement(Statement? statement) { + if (statement is Block) { + return _isLastStatementAnExitStatement(statement.lastStatement); + } else { + if (statement is BreakStatement) { + return statement.label == null; + } else if (statement is ContinueStatement) { + return statement.label == null; + } else if (statement is ReturnStatement) { + return true; + } + return statement != null && ExitDetector.exits(statement); + } + } + + /// If any of the variables is declared inside the for statement then it does + /// not mean anything afterwards. + bool _isRelevantOutsideOfForStatement(ForStatement node) { + if (breakScope.hasBreak(node)) { + return false; + } + + var loopParts = node.forLoopParts; + if (loopParts is ForParts) { + var condition = loopParts.condition; + if (condition == null) { + return false; + } + + // todo(pq): migrate away from `traverseNodesInDFS` (https://github.com/dart-lang/linter/issues/3745) + // ignore: deprecated_member_use_from_same_package + for (var ref in condition.traverseNodesInDFS()) { + if (ref is SimpleIdentifier) { + var element = ref.staticElement; + if (element == null) { + return false; + } + var refOffset = element.nameOffset; + if (refOffset > node.offset && refOffset < node.end) { + return false; + } + } + } + } + + return true; + } + + void _propagateUndefinedExpressions(ConditionScope? scope) { + if (scope != null) { + outerScope?.addAll(scope.getUndefinedExpressions()); + } + } + + ConditionScope? _removeLastScope() { + var deletedScope = outerScope; + outerScope = outerScope?.outer; + return deletedScope; + } + + ConditionScope? _visitElseStatement( + Statement? elseStatement, Expression condition) { + _addScope(); + _addFalseCondition(condition); + elseStatement?.accept(this); + return _removeLastScope(); + } + + void _visitIfStatement(IfStatement node) { + _addScope(); + node.expression.accept(this); + visitCondition(node.expression); + _addTrueCondition(node.expression); + node.thenStatement.accept(this); + _propagateUndefinedExpressions(_removeLastScope()); + } +} + +abstract class ExpressionBox { + bool haveToStop(Iterable elements); +} + +class _ConditionExpression extends ExpressionBox { + final Expression? expression; + final bool value; + + _ConditionExpression(this.expression, {this.value = true}); + + @override + bool haveToStop(Iterable elements) => false; + + @override + String toString() => '$expression is $value'; +} + +class _UndefinedAllExpression extends ExpressionBox { + @override + bool haveToStop(Iterable elements) => true; + + @override + String toString() => '*All* got undefined'; +} + +class _UndefinedExpression extends ExpressionBox { + final Element element; + + _UndefinedExpression._internal(this.element); + + @override + bool haveToStop(Iterable elements) => elements.contains(element); + + @override + String toString() => '$element got undefined'; + + static _UndefinedExpression? forElement(Element? element) { + var canonicalElement = element?.canonicalElement; + if (canonicalElement == null) return null; + return _UndefinedExpression._internal(canonicalElement); + } +} diff --git a/pkg/linter/lib/src/util/dart_type_utilities.dart b/pkg/linter/lib/src/util/dart_type_utilities.dart new file mode 100644 index 00000000000..8ab17ec8a7b --- /dev/null +++ b/pkg/linter/lib/src/util/dart_type_utilities.dart @@ -0,0 +1,326 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/dart/element/type.dart'; // ignore: implementation_imports + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; + +bool argumentsMatchParameters( + NodeList arguments, NodeList parameters) { + var namedParameters = {}; + var namedArguments = {}; + var positionalParameters = []; + var positionalArguments = []; + for (var parameter in parameters) { + var identifier = parameter.name; + if (identifier != null) { + if (parameter.isNamed) { + namedParameters[identifier.lexeme] = parameter.declaredElement; + } else { + positionalParameters.add(parameter.declaredElement); + } + } + } + for (var argument in arguments) { + if (argument is NamedExpression) { + var element = argument.expression.canonicalElement; + if (element == null) { + return false; + } + namedArguments[argument.name.label.name] = element; + } else { + var element = argument.canonicalElement; + if (element == null) { + return false; + } + positionalArguments.add(element); + } + } + if (positionalParameters.length != positionalArguments.length || + namedParameters.keys.length != namedArguments.keys.length) { + return false; + } + for (var i = 0; i < positionalArguments.length; i++) { + if (positionalArguments[i] != positionalParameters[i]) { + return false; + } + } + + for (var key in namedParameters.keys) { + if (namedParameters[key] != namedArguments[key]) { + return false; + } + } + + return true; +} + +/// Returns whether the canonical elements of [element1] and [element2] are +/// equal. +bool canonicalElementsAreEqual(Element? element1, Element? element2) => + element1?.canonicalElement == element2?.canonicalElement; + +/// Returns whether the canonical elements from two nodes are equal. +/// +/// As in, [NullableAstNodeExtension.canonicalElement], the two nodes must be +/// [Expression]s in order to be compared (otherwise `false` is returned). +/// +/// The two nodes must both be a [SimpleIdentifier], [PrefixedIdentifier], or +/// [PropertyAccess] (otherwise `false` is returned). +/// +/// If the two nodes are PrefixedIdentifiers, or PropertyAccess nodes, then +/// `true` is returned only if their canonical elements are equal, in +/// addition to their prefixes' and targets' (respectfully) canonical +/// elements. +/// +/// There is an inherent assumption about pure getters. For example: +/// +/// A a1 = ... +/// A a2 = ... +/// a1.b.c; // statement 1 +/// a2.b.c; // statement 2 +/// a1.b.c; // statement 3 +/// +/// The canonical elements from statements 1 and 2 are different, because a1 +/// is not the same element as a2. The canonical elements from statements 1 +/// and 3 are considered to be equal, even though `A.b` may have side effects +/// which alter the returned value. +bool canonicalElementsFromIdentifiersAreEqual( + Expression? rawExpression1, Expression? rawExpression2) { + if (rawExpression1 == null || rawExpression2 == null) return false; + + var expression1 = rawExpression1.unParenthesized; + var expression2 = rawExpression2.unParenthesized; + + if (expression1 is SimpleIdentifier) { + return expression2 is SimpleIdentifier && + canonicalElementsAreEqual(getWriteOrReadElement(expression1), + getWriteOrReadElement(expression2)); + } + + if (expression1 is PrefixedIdentifier) { + return expression2 is PrefixedIdentifier && + canonicalElementsAreEqual(expression1.prefix.staticElement, + expression2.prefix.staticElement) && + canonicalElementsAreEqual(getWriteOrReadElement(expression1.identifier), + getWriteOrReadElement(expression2.identifier)); + } + + if (expression1 is PropertyAccess && expression2 is PropertyAccess) { + var target1 = expression1.target; + var target2 = expression2.target; + return canonicalElementsFromIdentifiersAreEqual(target1, target2) && + canonicalElementsAreEqual( + getWriteOrReadElement(expression1.propertyName), + getWriteOrReadElement(expression2.propertyName)); + } + + return false; +} + +/// Returns whether [leftType] and [rightType] are _definitely_ unrelated. +/// +/// For the purposes of this function, here are some "relation" rules: +/// * `dynamic` and `Null` are considered related to any other type. +/// * Two types which are equal modulo nullability are considered related, +/// e.g. `int` and `int`, `String` and `String?`, `List` and +/// `List`, `List` and `List`, and type variables `A` and `A`. +/// * Two types such that one is a subtype of the other, modulo nullability, +/// such as `List` and `Iterable`, and type variables `A` +/// and `B` where `A extends B`, are considered related. +/// * Two interface types: +/// * are related if they represent the same class, modulo type arguments, +/// modulo nullability, and each of their pair-wise type arguments are +/// related, e.g. `List` and `List`, and `Future` and +/// `Future` where `S extends T`. +/// * are unrelated if [leftType]'s supertype is [Object]. +/// * are related if their supertypes are equal, e.g. `List` and +/// `Set`. +/// * Two type variables are related if their bounds are related. +/// * A record type is unrelated to any other type except a record type of +/// the same shape. +/// * Otherwise, any two types are related. +// TODO(srawlins): typedefs and functions in general. +bool typesAreUnrelated( + TypeSystem typeSystem, DartType? leftType, DartType? rightType) { + // If we don't have enough information, or can't really compare the types, + // return false as they _might_ be related. + if (leftType == null || + leftType.isBottom || + leftType is DynamicType || + rightType == null || + rightType.isBottom || + rightType is DynamicType) { + return false; + } + var promotedLeftType = typeSystem.promoteToNonNull(leftType); + var promotedRightType = typeSystem.promoteToNonNull(rightType); + if (promotedLeftType == promotedRightType || + typeSystem.isSubtypeOf(promotedLeftType, promotedRightType) || + typeSystem.isSubtypeOf(promotedRightType, promotedLeftType)) { + return false; + } + if (promotedLeftType is InterfaceType && promotedRightType is InterfaceType) { + return typeSystem.interfaceTypesAreUnrelated( + promotedLeftType, promotedRightType); + } else if (promotedLeftType is TypeParameterType && + promotedRightType is TypeParameterType) { + return typesAreUnrelated(typeSystem, promotedLeftType.element.bound, + promotedRightType.element.bound); + } else if (promotedLeftType is FunctionType) { + if (_isFunctionTypeUnrelatedToType(promotedLeftType, promotedRightType)) { + return true; + } + } else if (promotedRightType is FunctionType) { + if (_isFunctionTypeUnrelatedToType(promotedRightType, promotedLeftType)) { + return true; + } + } else if (promotedLeftType is RecordType || + promotedRightType is RecordType) { + return !typeSystem.isAssignableTo(promotedLeftType, promotedRightType) && + !typeSystem.isAssignableTo(promotedRightType, promotedLeftType); + } + return false; +} + +bool _isFunctionTypeUnrelatedToType(FunctionType type1, DartType type2) { + if (type2 is FunctionType) { + return false; + } + if (type2 is InterfaceType) { + var element2 = type2.element; + if (element2 is ClassElement && + element2.lookUpConcreteMethod('call', element2.library) != null) { + return false; + } + } + return true; +} + +typedef AstNodePredicate = bool Function(AstNode node); + +class DartTypeUtilities { + @Deprecated('Replace with `type.extendsClass`') + static bool extendsClass( + DartType? type, String? className, String? library) => + type.extendsClass(className, library!); + + @Deprecated('Replace with `rawNode.canonicalElement`') + static Element? getCanonicalElementFromIdentifier(AstNode? rawNode) => + rawNode.canonicalElement; + + @Deprecated('Replace with `type.implementsInterface`') + static bool implementsInterface( + DartType? type, String interface, String library) => + type.implementsInterface(interface, library); + + // todo(pq): remove and replace w/ an extension (pending internal migration) + @Deprecated('Slated for removal') + static bool isClass(DartType? type, String? className, String? library) => + type is InterfaceType && + type.element.name == className && + type.element.library.name == library; + + @Deprecated('Replace with `expression.isNullLiteral`') + static bool isNullLiteral(Expression? expression) => expression.isNullLiteral; + + @Deprecated('Use `argumentsMatchParameters`') + static bool matchesArgumentsWithParameters(NodeList arguments, + NodeList parameters) => + argumentsMatchParameters(arguments, parameters); + + @Deprecated('Replace with `node.traverseNodesInDFS`') + static Iterable traverseNodesInDFS(AstNode node, + {AstNodePredicate? excludeCriteria}) => + node.traverseNodesInDFS(excludeCriteria: excludeCriteria); +} + +class InterfaceTypeDefinition { + final String name; + final String library; + + InterfaceTypeDefinition(this.name, this.library); + + @override + int get hashCode => name.hashCode ^ library.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is InterfaceTypeDefinition && + name == other.name && + library == other.library; + } +} + +extension on TypeSystem { + bool interfaceTypesAreUnrelated( + InterfaceType leftType, InterfaceType rightType) { + var leftElement = leftType.element; + var rightElement = rightType.element; + if (leftElement == rightElement) { + // In this case, [leftElement] and [rightElement] represent the same + // class, modulo generics, e.g. `List` and `List`. Now we + // need to check type arguments. + var leftTypeArguments = leftType.typeArguments; + var rightTypeArguments = rightType.typeArguments; + if (leftTypeArguments.length != rightTypeArguments.length) { + // I cannot think of how we would enter this block, but it guards + // against RangeError below. + return false; + } + for (var i = 0; i < leftTypeArguments.length; i++) { + // If any of the pair-wise type arguments are unrelated, then + // [leftType] and [rightType] are unrelated. + if (typesAreUnrelated( + this, leftTypeArguments[i], rightTypeArguments[i])) { + return true; + } + } + // Otherwise, they might be related. + return false; + } else { + var sameSupertypes = leftElement.supertype == rightElement.supertype; + + // Unrelated Enums have the same supertype, but they are not the same element, so + // they are unrelated. + if (sameSupertypes && leftElement is EnumElement) { + return true; + } + + return (leftElement.supertype?.isDartCoreObject ?? false) || + !sameSupertypes; + } + } +} + +extension DartTypeExtensions on DartType { + /// Returns the type which should be used when conducting "interface checks" + /// on `this`. + /// + /// If `this` is a type variable, then the type-for-interface-check of its + /// promoted bound or bound is returned. Otherwise, `this` is returned. + // TODO(srawlins): Move to extensions.dart. + DartType get typeForInterfaceCheck { + var self = this; + if (self is TypeParameterType) { + if (self is TypeParameterTypeImpl) { + var promotedType = self.promotedBound; + if (promotedType != null) { + return promotedType.typeForInterfaceCheck; + } + } + return self.bound.typeForInterfaceCheck; + } else { + return self; + } + } +} diff --git a/pkg/linter/lib/src/util/flutter_utils.dart b/pkg/linter/lib/src/util/flutter_utils.dart new file mode 100644 index 00000000000..2e142446d7c --- /dev/null +++ b/pkg/linter/lib/src/util/flutter_utils.dart @@ -0,0 +1,150 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; + +import '../extensions.dart'; +import '../util/dart_type_utilities.dart'; + +var _collectionInterfaces = [ + InterfaceTypeDefinition('List', 'dart.core'), + InterfaceTypeDefinition('Map', 'dart.core'), + InterfaceTypeDefinition('LinkedHashMap', 'dart.collection'), + InterfaceTypeDefinition('Set', 'dart.core'), + InterfaceTypeDefinition('LinkedHashSet', 'dart.collection'), +]; + +_Flutter _flutterInstance = _Flutter('flutter', 'package:flutter'); + +_Flutter get _flutter => _flutterInstance; + +bool hasWidgetAsAscendant(ClassElement element) => + _flutter.hasWidgetAsAscendant(element); + +bool isBuildContext(DartType? type, {bool skipNullable = false}) => + _flutter.isBuildContext(type, skipNullable: skipNullable); + +bool isExactWidget(ClassElement element) => _flutter.isExactWidget(element); + +bool isExactWidgetTypeContainer(DartType? type) => + _flutter.isExactWidgetTypeContainer(type); + +bool isExactWidgetTypeSizedBox(DartType? type) => + _flutter.isExactWidgetTypeSizedBox(type); + +bool isKDebugMode(Element? element) => _flutter.isKDebugMode(element); + +bool isStatefulWidget(ClassElement? element) => + _flutter.isStatefulWidget(element); + +bool isWidgetProperty(DartType? type) { + if (isWidgetType(type)) { + return true; + } + if (type is InterfaceType && + type.implementsAnyInterface(_collectionInterfaces)) { + return type.element.typeParameters.length == 1 && + isWidgetProperty(type.typeArguments.first); + } + return false; +} + +bool isWidgetType(DartType? type) => _flutter.isWidgetType(type); + +/// See: analysis_server/lib/src/utilities/flutter.dart +class _Flutter { + static const _nameBuildContext = 'BuildContext'; + static const _nameStatefulWidget = 'StatefulWidget'; + static const _nameWidget = 'Widget'; + static const _nameContainer = 'Container'; + static const _nameSizedBox = 'SizedBox'; + + final String packageName; + final String widgetsUri; + + final Uri _uriBasic; + final Uri _uriContainer; + final Uri _uriFramework; + final Uri _uriFoundation; + + _Flutter(this.packageName, String uriPrefix) + : widgetsUri = '$uriPrefix/widgets.dart', + _uriBasic = Uri.parse('$uriPrefix/src/widgets/basic.dart'), + _uriContainer = Uri.parse('$uriPrefix/src/widgets/container.dart'), + _uriFramework = Uri.parse('$uriPrefix/src/widgets/framework.dart'), + _uriFoundation = Uri.parse('$uriPrefix/src/foundation/constants.dart'); + + bool hasWidgetAsAscendant(InterfaceElement? element, + [Set? alreadySeen]) { + alreadySeen ??= {}; + if (element == null || !alreadySeen.add(element)) { + return false; + } + if (_isExactWidget(element, _nameWidget, _uriFramework)) { + return true; + } + return hasWidgetAsAscendant(element.supertype?.element, alreadySeen); + } + + bool isBuildContext(DartType? type, {bool skipNullable = false}) { + if (type is! InterfaceType) { + return false; + } + if (skipNullable && type.nullabilitySuffix == NullabilitySuffix.question) { + return false; + } + return _isExactWidget(type.element, _nameBuildContext, _uriFramework); + } + + bool isExactWidget(ClassElement element) => + _isExactWidget(element, _nameWidget, _uriFramework); + + bool isExactWidgetTypeContainer(DartType? type) => + type is InterfaceType && + _isExactWidget(type.element, _nameContainer, _uriContainer); + + bool isExactWidgetTypeSizedBox(DartType? type) => + type is InterfaceType && + _isExactWidget(type.element, _nameSizedBox, _uriBasic); + + bool isKDebugMode(Element? element) => + element != null && + element.name == 'kDebugMode' && + element.source?.uri == _uriFoundation; + + bool isStatefulWidget(ClassElement? element) { + if (element == null) { + return false; + } + if (_isExactWidget(element, _nameStatefulWidget, _uriFramework)) { + return true; + } + for (var type in element.allSupertypes) { + if (_isExactWidget(type.element, _nameStatefulWidget, _uriFramework)) { + return true; + } + } + return false; + } + + bool isWidget(InterfaceElement element) { + if (_isExactWidget(element, _nameWidget, _uriFramework)) { + return true; + } + for (var type in element.allSupertypes) { + if (_isExactWidget(type.element, _nameWidget, _uriFramework)) { + return true; + } + } + return false; + } + + bool isWidgetType(DartType? type) => + type is InterfaceType && isWidget(type.element); + + bool _isExactWidget(InterfaceElement element, String type, Uri uri) => + element.name == type && element.source.uri == uri; +} diff --git a/pkg/linter/lib/src/util/leak_detector_visitor.dart b/pkg/linter/lib/src/util/leak_detector_visitor.dart new file mode 100644 index 00000000000..3ae635db701 --- /dev/null +++ b/pkg/linter/lib/src/util/leak_detector_visitor.dart @@ -0,0 +1,251 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:meta/meta.dart'; + +import '../analyzer.dart'; +import '../ast.dart'; +import '../extensions.dart'; + +/// Builds a function that reports a variable node if none of the predicates +/// which result from building [predicates] with [predicateBuilders] return +/// `true` for any node inside the [container] node. +_VisitVariableDeclaration _buildVariableReporter( + AstNode container, + Iterable<_PredicateBuilder> predicateBuilders, + LintRule rule, + Map predicates) => + (VariableDeclaration variable) { + var variableElement = variable.declaredElement; + if (variableElement == null) { + return; + } + + if (!predicates.keys + .any((DartTypePredicate p) => p(variableElement.type))) { + return; + } + + // todo(pq): migrate away from `traverseNodesInDFS` (https://github.com/dart-lang/linter/issues/3745) + // ignore: deprecated_member_use_from_same_package + var containerNodes = container.traverseNodesInDFS(); + + for (var predicateBuilder in predicateBuilders) { + if (containerNodes.any(predicateBuilder(variableElement))) { + return; + } + } + + if (_hasVariableAssignments(containerNodes, variable)) { + return; + } + + if (_hasNodesInvokingMethodOnVariable( + containerNodes, variable, predicates)) { + return; + } + + if (_hasMethodCallbackNodes( + containerNodes, variableElement, predicates)) { + return; + } + + // If any function is invoked with our variable, we suppress lints. This + // is because it is not so uncommon to invoke the target method there. We + // might not have access to the body of such function at analysis time, so + // trying to infer if the close method is invoked there is not always + // possible. + // TODO: Should there be another lint more relaxed that omits this step? + if (_hasMethodInvocationsWithVariableAsArgument( + containerNodes, variableElement)) { + return; + } + + rule.reportLint(variable); + }; + +/// Returns a predicate that returns true for a node `n` if `n` is a +/// [ConstructorFieldInitializer] initializing [v]. +_Predicate _hasConstructorFieldInitializers(VariableElement v) => (AstNode n) => + n is ConstructorFieldInitializer && n.fieldName.staticElement == v; + +/// Returns a predicate that returns true for a node `n` if `n` is a +/// [FieldFormalParameter] initializing [v]. +_Predicate _hasFieldFormalParameter(VariableElement v) => (AstNode n) { + if (n is! FieldFormalParameter) { + return false; + } + var staticElement = n.declaredElement; + return staticElement is FieldFormalParameterElement && + staticElement.field == v; + }; + +/// Whether any of the [predicates] holds true for [type] and [methodName]. +bool _hasMatch(Map predicates, DartType type, + String methodName) => + predicates.keys.any((p) => predicates[p] == methodName && p(type)); + +bool _hasMethodCallbackNodes( + Iterable containerNodes, + VariableElement variableElement, + Map predicates) { + var prefixedIdentifiers = containerNodes.whereType(); + return prefixedIdentifiers.any((n) => + n.prefix.staticElement == variableElement && + _hasMatch(predicates, variableElement.type, n.identifier.token.lexeme)); +} + +bool _hasMethodInvocationsWithVariableAsArgument( + Iterable containerNodes, VariableElement variableElement) { + var methodInvocations = containerNodes.whereType(); + return methodInvocations.any((n) => n.argumentList.arguments + .whereType() + .map((e) => e.staticElement) + .contains(variableElement)); +} + +bool _hasNodesInvokingMethodOnVariable( + Iterable classNodes, + VariableDeclaration variable, + Map predicates) => + classNodes.any((AstNode n) { + var declaredElement = variable.declaredElement; + return declaredElement != null && + n is MethodInvocation && + ((_hasMatch(predicates, declaredElement.type, n.methodName.name) && + (_isSimpleIdentifierElementEqualToVariable( + n.realTarget, declaredElement) || + _isPostfixExpressionOperandEqualToVariable( + n.realTarget, declaredElement) || + _isPropertyAccessThroughThis( + n.realTarget, declaredElement) || + (n.thisOrAncestorMatching((a) => a == variable) != + null))) || + (_isInvocationThroughCascadeExpression(n, declaredElement))); + }); + +/// Returns a predicate that returns true for a node `n` if `n` is a +/// [ReturnStatement] initializing [v]. +_Predicate _hasReturn(VariableElement v) => (AstNode n) { + if (n is! ReturnStatement) { + return false; + } + var expression = n.expression; + return expression is SimpleIdentifier && expression.staticElement == v; + }; + +bool _hasVariableAssignments( + Iterable containerNodes, VariableDeclaration variable) { + if (variable.equals != null && variable.initializer is SimpleIdentifier) { + return true; + } + + return containerNodes.any((n) => + n is AssignmentExpression && + (_isElementEqualToVariable(n.writeElement, variable.declaredElement) || + // Assignment to VariableDeclaration as setter. + (n.leftHandSide is PropertyAccess && + (n.leftHandSide as PropertyAccess).propertyName.token.lexeme == + variable.name.lexeme)) + // Being assigned another reference. + && + n.rightHandSide is SimpleIdentifier); +} + +bool _isElementEqualToVariable( + Element? propertyElement, VariableElement? variableElement) => + propertyElement == variableElement || + propertyElement is PropertyAccessorElement && + propertyElement.variable == variableElement; + +bool _isInvocationThroughCascadeExpression( + MethodInvocation invocation, VariableElement variableElement) { + if (invocation.realTarget is! SimpleIdentifier) { + return false; + } + + var identifier = invocation.realTarget; + if (identifier is SimpleIdentifier) { + var element = identifier.staticElement; + if (element is PropertyAccessorElement) { + return element.variable == variableElement; + } + } + return false; +} + +bool _isPostfixExpressionOperandEqualToVariable( + AstNode? n, VariableElement variableElement) { + if (n is PostfixExpression) { + var operand = n.operand; + return operand is SimpleIdentifier && + _isElementEqualToVariable(operand.staticElement, variableElement); + } + return false; +} + +bool _isPropertyAccessThroughThis( + Expression? n, VariableElement variableElement) { + if (n is! PropertyAccess) { + return false; + } + + var target = n.realTarget; + if (target is! ThisExpression) { + return false; + } + + var propertyElement = n.propertyName.staticElement; + return _isElementEqualToVariable(propertyElement, variableElement); +} + +bool _isSimpleIdentifierElementEqualToVariable( + AstNode? n, VariableElement variableElement) => + n is SimpleIdentifier && + _isElementEqualToVariable(n.staticElement, variableElement); + +typedef DartTypePredicate = bool Function(DartType type); + +typedef _Predicate = bool Function(AstNode node); + +typedef _PredicateBuilder = _Predicate Function(VariableElement v); + +typedef _VisitVariableDeclaration = void Function(VariableDeclaration node); + +abstract class LeakDetectorProcessors extends SimpleAstVisitor { + static final _variablePredicateBuilders = <_PredicateBuilder>[_hasReturn]; + static final _fieldPredicateBuilders = <_PredicateBuilder>[ + _hasConstructorFieldInitializers, + _hasFieldFormalParameter + ]; + + final LintRule rule; + + LeakDetectorProcessors(this.rule); + + @protected + Map get predicates; + + @override + void visitFieldDeclaration(FieldDeclaration node) { + var unit = getCompilationUnit(node); + if (unit != null) { + node.fields.variables.forEach(_buildVariableReporter( + unit, _fieldPredicateBuilders, rule, predicates)); + } + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + var function = node.thisOrAncestorOfType(); + if (function != null) { + node.variables.variables.forEach(_buildVariableReporter( + function, _variablePredicateBuilders, rule, predicates)); + } + } +} diff --git a/pkg/linter/lib/src/util/score_utils.dart b/pkg/linter/lib/src/util/score_utils.dart new file mode 100644 index 00000000000..143880c12f4 --- /dev/null +++ b/pkg/linter/lib/src/util/score_utils.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/config.dart'; // ignore: implementation_imports +import 'package:http/http.dart' as http; + +import '../utils.dart'; + +Future> _readCoreLints() async => _fetchLints( + 'https://raw.githubusercontent.com/dart-lang/lints/main/lib/core.yaml'); + +/// todo(pq): de-duplicate these fetches / URIs +Future> _readFlutterLints() async => _fetchLints( + 'https://raw.githubusercontent.com/flutter/packages/main/packages/flutter_lints/lib/flutter.yaml'); + +Future> _readRecommendedLints() async => _fetchLints( + 'https://raw.githubusercontent.com/dart-lang/lints/main/lib/recommended.yaml'); + +/// todo(pq): update `scorecard.dart` to reuse these fetch functions. +Future> _fetchLints(String url) async { + var req = await http.get(Uri.parse(url)); + return _readLints(req.body); +} + +List _readLints(String contents) { + var lintConfigs = processAnalysisOptionsFile(contents); + if (lintConfigs == null) { + return []; + } + return lintConfigs.ruleConfigs.map((c) => c.name ?? '').toList(); +} + +List? _coreRules; +List? _recommendedRules; +List? _flutterRules; + +Future> get coreRules async => + _coreRules ??= await _readCoreLints(); + +Future> get recommendedRules async => + _recommendedRules ??= await _readRecommendedLints(); + +Future> get flutterRules async => + _flutterRules ??= await _readFlutterLints(); + +Future> fetchRules(Uri optionsUrl) async { + var config = await _fetchConfig(optionsUrl); + if (config == null) { + printToConsole('no config found for: $optionsUrl (SKIPPED)'); + return []; + } + var rules = []; + for (var ruleConfig in config.ruleConfigs) { + var name = ruleConfig.name; + if (name != null) { + rules.add(name); + } + } + return rules; +} + +Future _fetchConfig(Uri url) async { + printToConsole('loading $url...'); + var req = await http.get(url); + return processAnalysisOptionsFile(req.body); +} diff --git a/pkg/linter/lib/src/util/unrelated_types_visitor.dart b/pkg/linter/lib/src/util/unrelated_types_visitor.dart new file mode 100644 index 00000000000..a5bfe0f6397 --- /dev/null +++ b/pkg/linter/lib/src/util/unrelated_types_visitor.dart @@ -0,0 +1,226 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/dart/element/type_provider.dart'; + +import '../analyzer.dart'; +import '../util/dart_type_utilities.dart'; + +/// Base class for visitor used in rules where we want to lint about invoking +/// methods on generic classes where the type of the singular argument is +/// unrelated to the singular type argument of the class. Extending this +/// visitor is as simple as knowing the methods, classes and libraries that +/// uniquely define the target, i.e. implement only [methods]. +abstract class UnrelatedTypesProcessors extends SimpleAstVisitor { + final LintRule rule; + final TypeSystem typeSystem; + final TypeProvider typeProvider; + + UnrelatedTypesProcessors(this.rule, this.typeSystem, this.typeProvider); + + /// The method definitions which this [UnrelatedTypesProcessors] is concerned + /// with. + List get methods; + + List get indexOperators => []; + + @override + void visitIndexExpression(IndexExpression node) { + var matchingMethods = + indexOperators.where((method) => '[]' == method.methodName); + if (matchingMethods.isEmpty) { + return; + } + + var targetType = _getTargetType(node, node.realTarget); + if (targetType is! InterfaceType) { + return; + } + + for (var methodDefinition in matchingMethods) { + var collectionType = methodDefinition.collectionTypeFor(targetType); + if (collectionType != null) { + _checkMethod(node.index, methodDefinition, collectionType); + return; + } + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.argumentList.arguments.length != 1) { + return; + } + + var matchingMethods = + methods.where((method) => node.methodName.name == method.methodName); + if (matchingMethods.isEmpty) { + return; + } + + // At this point, we know that [node] is an invocation of a method which + // has the same name as the method that this [UnrelatedTypesProcessors] is + // concerned with, and that the method call has a single argument. + // + // We've completed the "cheap" checks, and must now continue with the + // arduous task of determining whether the method target implements + // [definition]. + var targetType = _getTargetType(node, node.realTarget); + if (targetType is! InterfaceType) { + return; + } + + for (var methodDefinition in matchingMethods) { + var collectionType = methodDefinition.collectionTypeFor(targetType); + if (collectionType != null) { + _checkMethod(node.argumentList.arguments.first, methodDefinition, + collectionType); + return; + } + } + } + + DartType? _getTargetType(Expression node, Expression? target) { + if (target != null) { + return target.staticType; + } + + // Look for an implicit receiver, starting with [node]'s parent's parent. + for (AstNode? parent = node.parent?.parent; + parent != null; + parent = parent.parent) { + if (parent is ClassDeclaration) { + return parent.declaredElement?.thisType; + } else if (parent is MixinDeclaration) { + return parent.declaredElement?.thisType; + } else if (parent is EnumDeclaration) { + return parent.declaredElement?.thisType; + } else if (parent is ExtensionDeclaration) { + return parent.extendedType.type; + } + } + return null; + } + + /// Checks a [MethodInvocation] or [IndexExpression] which has a singular + /// [argument] and matches [methodDefinition], with a target with a static + /// type of [collectionType]. + void _checkMethod(Expression argument, MethodDefinition methodDefinition, + InterfaceType collectionType) { + // Finally, determine whether the type of the argument is related to the + // type of the method target. + var argumentType = argument.staticType; + if (argumentType == null) return; + + switch (methodDefinition.expectedArgumentKind) { + case ExpectedArgumentKind.assignableToCollectionTypeArgument: + var typeArgument = + collectionType.typeArguments[methodDefinition.typeArgumentIndex]; + if (typesAreUnrelated(typeSystem, argumentType, typeArgument)) { + rule.reportLint(argument, arguments: [ + argumentType.getDisplayString(withNullability: true), + typeArgument.getDisplayString(withNullability: true), + ]); + } + + case ExpectedArgumentKind.assignableToCollection: + if (!typeSystem.isAssignableTo(argumentType, collectionType)) { + rule.reportLint(argument, arguments: [ + argumentType.getDisplayString(withNullability: true), + collectionType.getDisplayString(withNullability: true), + ]); + } + + case ExpectedArgumentKind.assignableToIterableOfTypeArgument: + var iterableType = + collectionType.asInstanceOf(typeProvider.iterableElement); + if (iterableType != null && + !typeSystem.isAssignableTo(argumentType, iterableType)) { + rule.reportLint(argument, arguments: [ + argumentType.getDisplayString(withNullability: true), + iterableType.getDisplayString(withNullability: true), + ]); + } + } + } +} + +/// A definition of a method and the expected characteristics of the first +/// argument to any invocation. +abstract class MethodDefinition { + final String methodName; + + /// The index of the type argument which the method argument should match. + final int typeArgumentIndex; + + final ExpectedArgumentKind expectedArgumentKind; + + MethodDefinition( + this.methodName, + this.expectedArgumentKind, { + this.typeArgumentIndex = 0, + }); + + InterfaceType? collectionTypeFor(InterfaceType targetType); +} + +class MethodDefinitionForElement extends MethodDefinition { + /// The element on which this method is declared. + final ClassElement element; + + MethodDefinitionForElement( + this.element, + super.methodName, + super.expectedArgumentKind, { + super.typeArgumentIndex = 0, + }); + + @override + InterfaceType? collectionTypeFor(InterfaceType targetType) => + targetType.asInstanceOf(element); +} + +class MethodDefinitionForName extends MethodDefinition { + final String libraryName; + + final String interfaceName; + + MethodDefinitionForName( + this.libraryName, + this.interfaceName, + super.methodName, + super.expectedArgumentKind, { + super.typeArgumentIndex = 0, + }); + + @override + InterfaceType? collectionTypeFor(InterfaceType targetType) { + for (var supertype in [targetType, ...targetType.allSupertypes]) { + var element = supertype.element; + if (element.name == interfaceName && + element.library.name == libraryName) { + return targetType.asInstanceOf(element); + } + } + return null; + } +} + +/// The kind of the expected argument. +enum ExpectedArgumentKind { + /// An argument is expected to be assignable to a type argument on the + /// collection type. + assignableToCollectionTypeArgument, + + /// An argument is expected to be assignable to the collection type. + assignableToCollection, + + /// An argument is expected to be assignable to `Iterable` where `E` is the + /// (only) type argument on the collection type. + assignableToIterableOfTypeArgument, +} diff --git a/pkg/linter/lib/src/utils.dart b/pkg/linter/lib/src/utils.dart new file mode 100644 index 00000000000..68a1bacbfa2 --- /dev/null +++ b/pkg/linter/lib/src/utils.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/io.dart'; // ignore: implementation_imports + +import 'ast.dart'; +import 'util/ascii_utils.dart'; + +// An identifier here is defined as: +// * A sequence of `_`, `$`, letters or digits, +// * where no `$` comes after a digit. +final _identifier = RegExp(r'^[_$a-z]+(\d[_a-z\d]*)?$', caseSensitive: false); + +// A lower camel-case here is defined as: +// * Any number of optional leading underscores, +// * a lower case letter, `$` or `?` followed by a "word-tail" +// (a sequence of lower-case letters, digits, `$` or `?`), +// * followed by any number of either +// * an upper case letter followed by a word tail, or +// * an underscore and then a digit followed by a word tail. +// * and potentially ended by a single optional underscore. +final _lowerCamelCase = + RegExp(r'^_*[?$a-z][a-z\d?$]*(?:(?:[A-Z]|_\d)[a-z\d?$]*)*_?$'); + +// A lower-case underscore (snake-case) is here defined as: +// * A sequence of lower-case letters, digits and underscores, +// * starting with a lower-case letter, and +// * with no two adjacent underscores, +// * and not ending in an underscore. +final _lowerCaseUnderScore = RegExp(r'^[a-z](?:_?[a-z\d])*$'); + +@Deprecated('Prefer: ascii_utils.isValidFileName') +final _lowerCaseUnderScoreWithDots = + RegExp(r'^_?[_a-z\d]*(?:\.[a-z][_a-z\d]*)*$'); + +// A lower-case underscored (snake-case) with leading underscores is defined as +// * An optional leading sequence of any number of underscores, +// * followed by a sequence of lower-case letters, digits and underscores, +// * with no two adjacent underscores, +// * and not ending in an underscore. +final _lowerCaseUnderScoreWithLeadingUnderscores = + RegExp(r'^_*[a-z](?:_?[a-z\d])*$'); + +final _pubspec = RegExp(r'^_?pubspec\.yaml$'); + +// A library prefix here is defined as: +// * An optional leading `?`, +// * then any number of underscores, `_`, +// * then a lower-case letter, +// * followed by any number of lower-case letters, digits and underscores. +final _validLibraryPrefix = RegExp(r'^\$?_*[a-z][_a-z\d]*$'); + +/// Returns `true` if the given [name] has a leading `_`. +@Deprecated('Prefer: ascii_utils String extension `hasLeadingUnderscore`') +bool hasLeadingUnderscore(String name) => name.hasLeadingUnderscore; + +/// Check if this [string] is formatted in `CamelCase`. +bool isCamelCase(String string) => CamelCaseString.isCamelCase(string); + +/// Returns `true` if this [fileName] is a Dart file. +bool isDartFileName(String fileName) => fileName.endsWith('.dart'); + +/// Returns `true` if this [name] is a legal Dart identifier. +bool isIdentifier(String name) => _identifier.hasMatch(name); + +/// Returns `true` if this [id] is `lowerCamelCase`. +bool isLowerCamelCase(String id) => + id.length == 1 && isUpperCase(id.codeUnitAt(0)) || + id == '_' || + _lowerCamelCase.hasMatch(id); + +/// Returns `true` if this [id] is `lower_camel_case_with_underscores`. +bool isLowerCaseUnderScore(String id) => _lowerCaseUnderScore.hasMatch(id); + +/// Returns `true` if this [id] is `lower_camel_case_with_underscores_or.dots`. +bool isLowerCaseUnderScoreWithDots(String id) => + // ignore: deprecated_member_use_from_same_package + _lowerCaseUnderScoreWithDots.hasMatch(id); + +/// Returns `true` if this [fileName] is a Pubspec file. +bool isPubspecFileName(String fileName) => _pubspec.hasMatch(fileName); + +/// Returns `true` if the given code unit [c] is upper case. +bool isUpperCase(int c) => c >= 0x40 && c <= 0x5A; + +/// Returns true if this [libraryPrefix] is valid. +bool isValidLibraryPrefix(String libraryPrefix) => + _validLibraryPrefix.hasMatch(libraryPrefix); + +/// Returns true if this [id] is a valid package name. +bool isValidPackageName(String id) => + _lowerCaseUnderScoreWithLeadingUnderscores.hasMatch(id) && + isIdentifier(id) && + !isReservedWord(id); + +/// Write the given [object] to the console. +/// Uses the shared [outSink] for redirecting in tests. +void printToConsole(Object? object) { + outSink.writeln(object); +} + +class CamelCaseString { + static final _camelCaseMatcher = RegExp(r'[A-Z][a-z]*'); + + // A camel case string here is defined as: + // * An arbitrary number of optional leading `_`s or `$`s, + // * followed by an upper-case letter, `$` or `?`, + // * followed by any number of letters, digits, `?` or `$`s. + // + // This ensures that the text contains a `$`, `?` or upper-case letter + // before any lower-case letter or digit, and no letters or `?`s before an + // `_`. + static final _camelCaseTester = RegExp(r'^_*(?:\$+_+)*[$?A-Z][$?a-zA-Z\d]*$'); + + final String value; + CamelCaseString(this.value) { + if (!isCamelCase(value)) { + throw ArgumentError.value(value, 'value', '$value is not CamelCase'); + } + } + + String get humanized => _humanize(value); + + @override + String toString() => value; + + static bool isCamelCase(String name) => _camelCaseTester.hasMatch(name); + + static String _humanize(String camelCase) => + _camelCaseMatcher.allMatches(camelCase).map((m) => m[0]).join(' '); +} diff --git a/pkg/linter/pubspec.yaml b/pkg/linter/pubspec.yaml new file mode 100644 index 00000000000..3a1385fa420 --- /dev/null +++ b/pkg/linter/pubspec.yaml @@ -0,0 +1,30 @@ +name: linter +# This package is not intended for consumption on pub.dev. DO NOT publish. +publish_to: none + +description: >- + The implementation of the lint rules supported by the analyzer framework. + This package is not intended to be used directly. + +environment: + sdk: ^3.0.0 + +dependencies: + analyzer: ^6.2.0 + args: ^2.1.0 + collection: ^1.15.0 + http: ">=0.13.0 <2.0.0" + meta: ^1.3.0 + path: ^1.8.0 + pub_semver: ^2.0.0 + source_span: ^1.8.0 + yaml: ^3.0.0 + +dev_dependencies: + benchmark_harness: ^2.0.0 + cli_util: ^0.4.0 + lints: ^2.0.0 + markdown: ^7.0.0 + matcher: ^0.12.10 + test: ^1.16.1 + test_reflective_loader: ^0.2.0 diff --git a/pkg/linter/test/all.dart b/pkg/linter/test/all.dart new file mode 100644 index 00000000000..572682a6531 --- /dev/null +++ b/pkg/linter/test/all.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/io.dart'; + +import 'annotation_test.dart' as annotation_test; +import 'ascii_utils_test.dart' as ascii_utils_test; +import 'canonicalization_test.dart' as canonicalization_test; +import 'doc_test.dart' as doc_test; +import 'engine_test.dart' as engine_test; +import 'formatter_test.dart' as formatter_test; +import 'integration_test.dart' as integration_test; +import 'mocks.dart'; +import 'pubspec_test.dart' as pubspec_test; +import 'rule_test.dart' as rule_test; +import 'rules/all.dart' as reflective_rule_tests; +import 'unmocked_sdk_rule_test.dart' as unmocked_sdk_rule_test; +import 'utils_test.dart' as utils_test; +import 'validate_format_test.dart' as validate_format; +import 'validate_headers_test.dart' as validate_headers; +import 'validate_incompatible_rules.dart' as validate_incompatible_rules; +import 'validate_no_rule_description_references.dart' + as validate_no_rule_description_references; +import 'validate_rule_description_format_test.dart' + as validate_rule_description_format; +import 'validate_sdk_version_map.dart' as validate_sdk_version_map; +import 'verify_checks_test.dart' as verify_checks; +import 'verify_machine_json_test.dart' as verify_machine_json; +import 'verify_reflective_test_suites.dart' as verify_reflective_test_suites; + +void main() { + // Redirect output. + outSink = MockIOSink(); + + annotation_test.main(); + ascii_utils_test.main(); + canonicalization_test.main(); + doc_test.main(); + engine_test.main(); + formatter_test.main(); + integration_test.main(); + pubspec_test.main(); + rule_test.main(); + reflective_rule_tests.main(); + unmocked_sdk_rule_test.main(); + utils_test.main(); + validate_format.main(); + validate_headers.main(); + validate_incompatible_rules.main(); + validate_no_rule_description_references.main(); + validate_rule_description_format.main(); + validate_sdk_version_map.main(); + verify_checks.main(); + verify_machine_json.main(); + verify_reflective_test_suites.main(); +} diff --git a/pkg/linter/test/annotation_test.dart b/pkg/linter/test/annotation_test.dart new file mode 100644 index 00000000000..0a1c4f0749d --- /dev/null +++ b/pkg/linter/test/annotation_test.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/error/error.dart'; +import 'package:linter/src/test_utilities/annotation.dart'; +import 'package:test/test.dart'; + +import 'util/annotation_matcher.dart'; + +void main() { + test('extraction', () { + expect(extractAnnotation(1, 'int x; // LINT [1:3]'), isNotNull); + expect(extractAnnotation(1, 'int x; //LINT'), isNotNull); + expect(extractAnnotation(1, 'int x; // OK'), isNull); + expect(extractAnnotation(1, 'int x;'), isNull); + expect(extractAnnotation(1, 'dynamic x; // LINT dynamic is bad')!.message, + 'dynamic is bad'); + expect( + extractAnnotation(1, 'dynamic x; // LINT dynamic is bad')!.lineNumber, + 1); + expect( + extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad')! + .message, + 'dynamic is bad'); + expect( + extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad')!.column, + 1); + expect( + extractAnnotation(1, 'dynamic x; // LINT [1:3] dynamic is bad')!.length, + 3); + expect(extractAnnotation(1, 'dynamic x; //LINT')!.message, isNull); + expect(extractAnnotation(1, 'dynamic x; //LINT ')!.message, isNull); + // Commented out lines shouldn't get linted. + expect(extractAnnotation(1, '// dynamic x; //LINT '), isNull); + expect(extractAnnotation(1, 'int x; // LINT [2:3]')!.lineNumber, 1); + expect(extractAnnotation(1, 'int x; // LINT [2:3]')!.column, 2); + expect(extractAnnotation(1, 'int x; // LINT [2:3]')!.length, 3); + expect(extractAnnotation(1, 'int x; // LINT [+2]')!.lineNumber, 3); + expect(extractAnnotation(1, 'int x; // LINT [+2]')!.column, isNull); + expect(extractAnnotation(1, 'int x; // LINT [+2]')!.length, isNull); + expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]')!.lineNumber, 3); + expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]')!.column, 4); + expect(extractAnnotation(1, 'int x; // LINT [+2,4:5]')!.length, 5); + expect(extractAnnotation(10, 'int x; // LINT [-2]')!.lineNumber, 8); + }); + + test('equality', () { + expect(Annotation('Actual message (to be ignored)', ErrorType.LINT, 1), + matchesAnnotation(null, ErrorType.LINT, 1)); + expect(Annotation('Message', ErrorType.LINT, 1), + matchesAnnotation('Message', ErrorType.LINT, 1)); + }); + + test('inequality', () { + expect( + () => expect(Annotation('Message', ErrorType.LINT, 1), + matchesAnnotation('Message', ErrorType.HINT, 1)), + throwsA(TypeMatcher())); + expect( + () => expect(Annotation('Message', ErrorType.LINT, 1), + matchesAnnotation('Message2', ErrorType.LINT, 1)), + throwsA(TypeMatcher())); + expect( + () => expect(Annotation('Message', ErrorType.LINT, 1), + matchesAnnotation('Message', ErrorType.LINT, 2)), + throwsA(TypeMatcher())); + }); +} diff --git a/pkg/linter/test/ascii_utils_test.dart b/pkg/linter/test/ascii_utils_test.dart new file mode 100644 index 00000000000..e9536be3b62 --- /dev/null +++ b/pkg/linter/test/ascii_utils_test.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/util/ascii_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('fileNames', () { + group('good', () { + for (var name in goodFileNames) { + test(name, () { + expect(isValidDartFileName(name), isTrue); + }); + } + }); + + group('bad', () { + for (var name in badFileNames) { + test(name, () { + expect(isValidDartFileName(name), isFalse); + }); + } + }); + }); + + group('isJustUnderscores', () { + test( + 'empty is not', + () => expect(''.isJustUnderscores, isFalse), + ); + + test( + 'non-underscore is not', + () => expect('A'.isJustUnderscores, isFalse), + ); + + test( + 'one underscore is', + () => expect('_'.isJustUnderscores, isTrue), + ); + + test( + 'multiple underscores is', + () => expect('___'.isJustUnderscores, isTrue), + ); + + test( + 'underscores followed by non-underscores is not', + () => expect('__x'.isJustUnderscores, isFalse), + ); + }); +} + +final badFileNames = [ + 'Foo.dart', + 'fooBar.dart', + '.foo_Bar.dart', + 'F_B.dart', + 'JS.dart', + 'JSON.dart', +]; + +final goodFileNames = [ + // Generated files. + 'file-system.g.dart', + 'SliderMenu.css.dart', + '_file.dart', + '_file.g.dart', + // Non-strict Dart. + 'bwu_server.shared.datastore.some_file', + 'foo_bar.baz', + 'foo_bar.dart', + 'foo_bar.g.dart', + 'foo_bar', + 'foo.bar', + 'foo_bar_baz', + 'foo', + 'foo_', + 'foo.bar_baz.bang', + //See: https://github.com/flutter/flutter/pull/1996 + 'pointycastle.impl.ec_domain_parameters.gostr3410_2001_cryptopro_a', + 'a.b', + 'a.b.c', + 'p2.src.acme', + //See: https://github.com/dart-lang/linter/issues/1803 + '_', + '_f', + '__f', + '___f', + 'Foo', + 'fooBar.', + '.foo_Bar', + '_.', + '.', + 'F_B', + 'JS', + 'JSON', +]; diff --git a/pkg/linter/test/canonicalization_test.dart b/pkg/linter/test/canonicalization_test.dart new file mode 100644 index 00000000000..ac00ea8fa2f --- /dev/null +++ b/pkg/linter/test/canonicalization_test.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import 'rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CanonicalizationTest); + }); +} + +@reflectiveTest +class CanonicalizationTest extends PubPackageResolutionTest { + // ignore: non_constant_identifier_names + test_canonicalizedPathResolution() async { + /// https://github.com/dart-lang/linter/commit/fcb8c9093c9c7a7cfabe27e17d40bd09d4c408f7 + newFile('$testPackageLibPath/lib3.dart', r''' +C g() => C(); + +class C {} +'''); + newFile('$testPackageLibPath/lib2.dart', r''' +import 'lib3.dart'; + +void f(C c) {} +'''); + + await assertNoDiagnostics(r''' +import 'package:test/lib2.dart'; + +import 'lib3.dart'; + +void test() { + f(g()); +} +'''); + } +} diff --git a/pkg/linter/test/configs/README b/pkg/linter/test/configs/README new file mode 100644 index 00000000000..7a105472b38 --- /dev/null +++ b/pkg/linter/test/configs/README @@ -0,0 +1,11 @@ +Add directories here to define configurations for testing analysis options variations. + +For example: + + configs/ + control-flow-collections/ + analysis_options.yaml + spread-collections/ + analysis_options.yaml + +See the 'experiments' test group in rule_test.dart. diff --git a/pkg/linter/test/doc_test.dart b/pkg/linter/test/doc_test.dart new file mode 100644 index 00000000000..878c2f26d63 --- /dev/null +++ b/pkg/linter/test/doc_test.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +import '../tool/machine.dart'; +import 'util/test_utils.dart'; + +void main() { + group('doc generation', () { + setUp(setUpSharedTestEnvironment); + test('fixStatus (sanity)', () async { + var fixStatusMap = await fetchFixStatusMap(); + // Doc generation reads the fix status map to associate fix status + // badges with rule documentation. Here we check one for sanity. + // If the file moves or format changes, we'd expect this to fail. + expect(fixStatusMap['always_declare_return_types'], 'hasFix'); + }); + }); +} diff --git a/pkg/linter/test/engine_test.dart b/pkg/linter/test/engine_test.dart new file mode 100644 index 00000000000..0c6f59f2971 --- /dev/null +++ b/pkg/linter/test/engine_test.dart @@ -0,0 +1,225 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/dart/ast/ast.dart' show AstNode, AstVisitor; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/src/error/codes.dart'; +import 'package:analyzer/src/lint/io.dart'; +import 'package:analyzer/src/lint/linter.dart' hide CamelCaseString; +import 'package:analyzer/src/lint/pub.dart'; +import 'package:analyzer/src/string_source.dart' show StringSource; +import 'package:cli_util/cli_util.dart' show getSdkPath; +import 'package:linter/src/cli.dart' as cli; +import 'package:linter/src/utils.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'test_constants.dart'; +import 'util/test_utils.dart'; + +void main() { + defineLinterEngineTests(); +} + +/// Linter engine tests +void defineLinterEngineTests() { + group('engine', () { + setUp(setUpSharedTestEnvironment); + group('reporter', () { + void doTest( + String label, String expected, Function(PrintingReporter r) report) { + test(label, () { + String? msg; + var reporter = PrintingReporter((m) => msg = m); + report(reporter); + expect(msg, expected); + }); + } + + doTest('exception', 'EXCEPTION: LinterException: foo', + (r) => r.exception(LinterException('foo'))); + doTest('warn', 'WARN: foo', (r) => r.warn('foo')); + }); + + group('exceptions', () { + test('message', () { + expect(const LinterException('foo').message, 'foo'); + }); + test('toString', () { + expect(const LinterException().toString(), 'LinterException'); + expect(const LinterException('foo').toString(), 'LinterException: foo'); + }); + }); + + group('camel case', () { + test('humanize', () { + expect(CamelCaseString('FooBar').humanized, 'Foo Bar'); + expect(CamelCaseString('Foo').humanized, 'Foo'); + }); + test('validation', () { + expect(() => CamelCaseString('foo'), + throwsA(TypeMatcher())); + }); + test('toString', () { + expect(CamelCaseString('FooBar').toString(), 'FooBar'); + }); + }); + + group('groups', () { + test('factory', () { + expect(Group('style').custom, isFalse); + expect(Group('pub').custom, isFalse); + expect(Group('errors').custom, isFalse); + expect(Group('Kustom').custom, isTrue); + }); + test('builtins', () { + expect(Group.builtin.contains(Group.style), isTrue); + expect(Group.builtin.contains(Group.errors), isTrue); + expect(Group.builtin.contains(Group.pub), isTrue); + }); + }); + + group('lint driver', () { + test('pubspec', () { + bool? visited; + var options = LinterOptions([MockLinter((n) => visited = true)]); + SourceLinter(options).lintPubspecSource(contents: 'name: foo_bar'); + expect(visited, isTrue); + }); + test('error collecting', () { + var error = AnalysisError.tmp( + source: StringSource('foo', ''), + offset: 0, + length: 0, + errorCode: LintCode('MockLint', 'This is a test...')); + var linter = SourceLinter(LinterOptions([]))..onError(error); + expect(linter.errors.contains(error), isTrue); + }); + }); + + group('main', () { + setUp(() { + exitCode = 0; + errorSink = MockIOSink(); + }); + tearDown(() { + exitCode = 0; + errorSink = stderr; + }); + test('smoke', () async { + var firstRuleTest = + Directory(ruleTestDataDir).listSync().firstWhere(isDartFile); + await cli.run([firstRuleTest.path]); + expect(cli.isLinterErrorCode(exitCode), isFalse); + }); + test('no args', () async { + await cli.run([]); + expect(exitCode, cli.unableToProcessExitCode); + }); + test('help', () async { + await cli.run(['-h']); + // Help shouldn't generate an error code + expect(cli.isLinterErrorCode(exitCode), isFalse); + }); + test('unknown arg', () async { + await cli.run(['-XXXXX']); + expect(exitCode, cli.unableToProcessExitCode); + }); + test('custom sdk path', () async { + // Smoke test to ensure a custom sdk path doesn't sink the ship + var firstRuleTest = + Directory(ruleTestDataDir).listSync().firstWhere(isDartFile); + var sdk = getSdkPath(); + await cli.run(['--dart-sdk', sdk, firstRuleTest.path]); + expect(cli.isLinterErrorCode(exitCode), isFalse); + }); + }); + + group('dtos', () { + group('hyperlink', () { + test('html', () { + var link = Hyperlink('dart', 'http://dart.dev'); + expect(link.html, 'dart'); + }); + test('html - strong', () { + var link = Hyperlink('dart', 'http://dart.dev', bold: true); + expect( + link.html, 'dart'); + }); + }); + + group('rule', () { + test('comparing', () { + LintRule r1 = MockLintRule('Bar', Group('acme')); + LintRule r2 = MockLintRule('Foo', Group('acme')); + expect(r1.compareTo(r2), -1); + LintRule r3 = MockLintRule('Bar', Group('acme')); + LintRule r4 = MockLintRule('Bar', Group('woody')); + expect(r3.compareTo(r4), -1); + }); + }); + group('maturity', () { + test('comparing', () { + // Custom + var m1 = Maturity('foo', ordinal: 0); + var m2 = Maturity('bar', ordinal: 1); + expect(m1.compareTo(m2), -1); + // Builtin + expect(Maturity.stable.compareTo(Maturity.experimental), -1); + }); + }); + }); + }); +} + +typedef NodeVisitor = void Function(Object node); + +class MockLinter extends LintRule { + final NodeVisitor nodeVisitor; + MockLinter(this.nodeVisitor) + : super( + name: 'MockLint', + group: Group.style, + description: 'Desc', + details: 'And so on...'); + + @override + PubspecVisitor getPubspecVisitor() => MockPubspecVisitor(nodeVisitor); + + @override + AstVisitor getVisitor() => MockVisitor(nodeVisitor); +} + +class MockLintRule extends LintRule { + MockLintRule(String name, Group group) + : super(name: name, group: group, description: '', details: ''); + + @override + AstVisitor getVisitor() => MockVisitor(null); +} + +class MockPubspecVisitor extends PubspecVisitor { + final NodeVisitor? nodeVisitor; + + MockPubspecVisitor(this.nodeVisitor); + + @override + void visitPackageName(PSEntry node) { + nodeVisitor?.call(node); + } +} + +class MockVisitor extends GeneralizingAstVisitor { + final NodeVisitor? nodeVisitor; + + MockVisitor(this.nodeVisitor); + + @override + void visitNode(AstNode node) { + nodeVisitor?.call(node); + } +} diff --git a/pkg/linter/test/experiments_test.dart b/pkg/linter/test/experiments_test.dart new file mode 100644 index 00000000000..e8fe7ae4134 --- /dev/null +++ b/pkg/linter/test/experiments_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import '../test_data/rules/experiments/experiments.dart'; +import 'rule_test.dart'; +import 'test_constants.dart'; + +void main() { + group('experiments', () { + registerLintRuleExperiments(); + + for (var entry + in Directory(p.join(ruleTestDataDir, 'experiments')).listSync()) { + if (entry is! Directory) continue; + + group(p.basename(entry.path), () { + var analysisOptionsFile = + File(p.join(entry.path, 'analysis_options.yaml')); + var analysisOptions = analysisOptionsFile.readAsStringSync(); + var ruleTestDir = Directory(p.join(entry.path, 'rules')); + for (var test in ruleTestDir.listSync()) { + if (test is! File) continue; + var testFile = test; + var ruleName = p.basenameWithoutExtension(test.path); + if (ruleName.startsWith('.')) continue; + testRule(ruleName, testFile, analysisOptions: analysisOptions); + } + }); + } + }); +} diff --git a/pkg/linter/test/formatter_test.dart b/pkg/linter/test/formatter_test.dart new file mode 100644 index 00000000000..31bcc4586a8 --- /dev/null +++ b/pkg/linter/test/formatter_test.dart @@ -0,0 +1,135 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/formatter.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; + +void main() { + defineTests(); +} + +void defineTests() { + group('formatter', () { + test('shorten', () { + expect(shorten('/foo/bar', '/foo/bar/baz'), '/baz'); + }); + + test('pluralize', () { + expect(pluralize('issue', 0), '0 issues'); + expect(pluralize('issue', 1), '1 issue'); + expect(pluralize('issue', 2), '2 issues'); + }); + + group('reporter', () { + var lineInfo = LineInfo([3, 6, 9]); + + var type = MockErrorType()..displayName = 'test'; + + var code = TestErrorCode('mock_code', 'MSG')..type = type; + + var source = MockSource('/foo/bar/baz.dart'); + + var error = AnalysisError.tmp( + source: source, offset: 10, length: 3, errorCode: code); + + var info = AnalysisErrorInfoImpl([error], lineInfo); + + var out = CollectingSink(); + + var reporter = + SimpleFormatter([info], null, out, fileCount: 1, elapsedMs: 13) + ..write(); + + test('count', () { + expect(reporter.errorCount, 1); + }); + + test('write', () { + expect(out.buffer.toString().trim(), '''/foo/bar/baz.dart 3:2 [test] MSG + +1 file analyzed, 1 issue found, in 13 ms.'''); + }); + + test('stats', () { + out.buffer.clear(); + SimpleFormatter([info], null, out, + fileCount: 1, showStatistics: true, elapsedMs: 13) + .write(); + expect(out.buffer.toString(), + startsWith('''/foo/bar/baz.dart 3:2 [test] MSG + +1 file analyzed, 1 issue found, in 13 ms. + +----------------------------------------- +Counts +----------------------------------------- +mock_code 1 +----------------------------------------- +''')); + }); + }); + + group('reporter', () { + var lineInfo = LineInfo([3, 6, 9]); + + var type = MockErrorType()..displayName = 'test'; + + var code = TestErrorCode('MockError', 'MSG') + ..errorSeverity = ErrorSeverity('MockErrorSeverity', 0, '', '') + ..type = type; + + var source = MockSource('/foo/bar/baz.dart'); + + var error = AnalysisError.tmp( + source: source, offset: 12, length: 13, errorCode: code); + + var info = AnalysisErrorInfoImpl([error], lineInfo); + + var out = CollectingSink(); + + group('filtered', () { + var reporter = SimpleFormatter([info], _RejectingFilter(), out, + fileCount: 1, elapsedMs: 13) + ..write(); + + test('error count', () { + expect(reporter.errorCount, 0); + }); + + test('filter count', () { + expect(reporter.filteredLintCount, 1); + }); + + test('write', () { + expect(out.buffer.toString().trim(), + '1 file analyzed, 0 issues found (1 filtered), in 13 ms.'); + }); + }); + + group('machine-output', () { + test('write', () { + out.buffer.clear(); + SimpleFormatter([info], null, out, + fileCount: 1, machineOutput: true, elapsedMs: 13) + .write(); + + expect(out.buffer.toString().trim(), + '''MockErrorSeverity|MockErrorType|MockError|/foo/bar/baz.dart|3|4|13|MSG + +1 file analyzed, 1 issue found, in 13 ms.'''); + }); + }); + }); + }); +} + +class _RejectingFilter extends LintFilter { + @override + bool filter(AnalysisError lint) => true; +} diff --git a/pkg/linter/test/integration/avoid_web_libraries_in_flutter.dart b/pkg/linter/test/integration/avoid_web_libraries_in_flutter.dart new file mode 100644 index 00000000000..ca48ec3573f --- /dev/null +++ b/pkg/linter/test/integration/avoid_web_libraries_in_flutter.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/src/lint/io.dart'; +import 'package:analyzer/src/lint/linter.dart'; +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/cli.dart' as cli; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../test_constants.dart'; + +void main() { + group('avoid_web_libraries_in_flutter', () { + var currentOut = outSink; + var collectingOut = CollectingSink(); + setUp(() { + exitCode = 0; + outSink = collectingOut; + }); + tearDown(() { + collectingOut.buffer.clear(); + outSink = currentOut; + exitCode = 0; + }); + + test('no pubspec', () async { + await cli.runLinter([ + '$integrationTestDir/avoid_web_libraries_in_flutter/no_pubspec', + '--rules=avoid_web_libraries_in_flutter', + ], LinterOptions()); + expect(collectingOut.trim(), + contains('1 file analyzed, 0 issues found, in')); + expect(exitCode, 0); + }); + + test('non flutter app', () async { + await cli.runLinter([ + '$integrationTestDir/avoid_web_libraries_in_flutter/non_flutter_app', + '--rules=avoid_web_libraries_in_flutter', + ], LinterOptions()); + expect(collectingOut.trim(), + contains('2 files analyzed, 0 issues found, in')); + expect(exitCode, 0); + }); + + test('non web app', () async { + await cli.runLinter([ + '$integrationTestDir/avoid_web_libraries_in_flutter/non_web_app', + '--rules=avoid_web_libraries_in_flutter', + ], LinterOptions()); + expect(collectingOut.trim(), + contains('3 files analyzed, 3 issues found, in')); + expect(exitCode, 1); + }); + + test('web app', () async { + await cli.runLinter([ + '$integrationTestDir/avoid_web_libraries_in_flutter/web_app', + '--rules=avoid_web_libraries_in_flutter', + ], LinterOptions()); + expect(collectingOut.trim(), + contains('2 files analyzed, 3 issues found, in')); + expect(exitCode, 1); + }); + + test('web plugin', () async { + await cli.runLinter([ + '$integrationTestDir/avoid_web_libraries_in_flutter/web_plugin', + '--rules=avoid_web_libraries_in_flutter', + ], LinterOptions()); + expect(collectingOut.trim(), + contains('2 files analyzed, 0 issues found, in')); + expect(exitCode, 0); + }); + }); +} diff --git a/pkg/linter/test/integration/close_sinks.dart b/pkg/linter/test/integration/close_sinks.dart new file mode 100644 index 00000000000..757ba35c52a --- /dev/null +++ b/pkg/linter/test/integration/close_sinks.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/src/lint/io.dart'; +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:linter/src/cli.dart' as cli; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../test_constants.dart'; + +void main() { + group('close_sinks', () { + var currentOut = outSink; + var collectingOut = CollectingSink(); + setUp(() { + noSoundNullSafety = false; + exitCode = 0; + outSink = collectingOut; + }); + tearDown(() { + noSoundNullSafety = true; + collectingOut.buffer.clear(); + outSink = currentOut; + exitCode = 0; + }); + + test('close sinks', () async { + await cli.run(['$integrationTestDir/close_sinks', '--rules=close_sinks']); + expect( + collectingOut.trim(), + stringContainsInOrder([ + 'IOSink _sinkA; // LINT', + 'IOSink _sinkSomeFunction; // LINT', + '1 file analyzed, 2 issues found, in' + ])); + expect(exitCode, 1); + }); + }); +} diff --git a/pkg/linter/test/integration_test.dart b/pkg/linter/test/integration_test.dart new file mode 100644 index 00000000000..58421b6d2c0 --- /dev/null +++ b/pkg/linter/test/integration_test.dart @@ -0,0 +1,140 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/src/lint/io.dart'; +import 'package:analyzer/src/lint/state.dart'; +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/cli.dart' as cli; +import 'package:linter/src/rules.dart'; +import 'package:linter/src/utils.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +import '../test_data/rules/experiments/experiments.dart'; +import 'integration/avoid_web_libraries_in_flutter.dart' + as avoid_web_libraries_in_flutter; +import 'integration/close_sinks.dart' as close_sinks; +import 'mocks.dart'; +import 'test_constants.dart'; + +void main() { + group('integration', () { + ruleTests(); + coreTests(); + }); +} + +void coreTests() { + group('core', () { + group('config', () { + var currentOut = outSink; + var collectingOut = CollectingSink(); + setUp(() { + exitCode = 0; + outSink = collectingOut; + }); + tearDown(() { + collectingOut.buffer.clear(); + outSink = currentOut; + exitCode = 0; + }); + test('excludes', () async { + await cli.run([ + '$integrationTestDir/p2', + '-c', + '$integrationTestDir/p2/lintconfig.yaml' + ]); + expect( + collectingOut.trim(), + stringContainsInOrder( + ['4 files analyzed, 1 issue found (2 filtered), in'])); + expect(exitCode, 1); + }); + test('overrides', () async { + await cli.run([ + '$integrationTestDir/p2', + '-c', + '$integrationTestDir/p2/lintconfig2.yaml' + ]); + expect(collectingOut.trim(), + stringContainsInOrder(['4 files analyzed, 0 issues found, in'])); + expect(exitCode, 0); + }); + test('default', () async { + await cli.run(['$integrationTestDir/p2']); + expect(collectingOut.trim(), + stringContainsInOrder(['4 files analyzed, 3 issues found, in'])); + expect(exitCode, 1); + }); + }); + + group('examples', () { + test('all.yaml', () { + var src = readFile('example/all.yaml'); + + var options = _getOptionsFromString(src); + var configuredLints = + // ignore: cast_nullable_to_non_nullable + (options['linter'] as YamlMap)['rules'] as YamlList; + + // rules are sorted + expect( + configuredLints, orderedEquals(configuredLints.toList()..sort())); + + registerLintRules(); + + var registered = Analyzer.facade.registeredRules + .where((r) => + !r.state.isDeprecated && + !r.state.isRemoved && + !experiments.contains(r)) + .map((r) => r.name); + + for (var l in configuredLints) { + if (!registered.contains(l)) { + printToConsole(l); + } + } + + expect(configuredLints, unorderedEquals(registered)); + }); + }); + }); +} + +void ruleTests() { + group('rule', () { + avoid_web_libraries_in_flutter.main(); + close_sinks.main(); + }); +} + +/// Provide the options found in [optionsSource]. +Map _getOptionsFromString(String optionsSource) { + var options = {}; + var doc = loadYamlNode(optionsSource); + + // Empty options. + if (doc is YamlScalar && doc.value == null) { + return options; + } + if (doc is! YamlMap) { + throw Exception( + 'Bad options file format (expected map, got ${doc.runtimeType})'); + } + doc.nodes.forEach((k, YamlNode v) { + Object? key; + if (k is YamlScalar) { + key = k.value; + } + if (key is! String) { + throw Exception('Bad options file format (expected String scope key, ' + 'got ${k.runtimeType})'); + } + options[key] = v; + }); + return options; +} diff --git a/pkg/linter/test/mocks.dart b/pkg/linter/test/mocks.dart new file mode 100644 index 00000000000..1fb7c7ec8ed --- /dev/null +++ b/pkg/linter/test/mocks.dart @@ -0,0 +1,134 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer/src/lint/linter.dart'; + +class CollectingSink extends MockIOSink { + final StringBuffer buffer = StringBuffer(); + + @override + String toString() => buffer.toString(); + + String trim() => toString().trim(); + + @override + void write(obj) { + buffer.write(obj); + } + + @override + void writeln([Object? obj = '']) { + buffer.writeln(obj); + } +} + +class MockErrorType implements ErrorType { + @override + late String displayName; + + @override + late String name; + + @override + late int ordinal; + + @override + late ErrorSeverity severity; + + @override + int compareTo(ErrorType other) => 0; + + @override + String toString() => 'MockErrorType'; +} + +class MockIOSink implements IOSink { + @override + late Encoding encoding; + + @override + Future get done => Future.value(); + + @override + void add(List data) {} + + @override + void addError(Object error, [StackTrace? stackTrace]) {} + + @override + Future addStream(Stream> stream) => Future.value(); + + @override + Future close() => Future.value(); + + @override + Future flush() => Future.value(); + + @override + void write(Object? obj) {} + + @override + void writeAll(Iterable objects, [String separator = '']) {} + + @override + void writeCharCode(int charCode) {} + + @override + void writeln([Object? obj = '']) {} +} + +class MockReporter extends Reporter { + List exceptions = []; + + List warnings = []; + + MockReporter(); + + @override + void exception(LinterException exception) { + exceptions.add(exception); + } + + @override + void warn(String message) { + warnings.add(message); + } +} + +class MockSource extends BasicSource { + @override + final String fullName; + + MockSource(this.fullName) : super(Uri.file(fullName)); + + @override + TimestampedData get contents => TimestampedData(0, ''); + + @override + bool exists() => false; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class TestErrorCode extends ErrorCode { + @override + late ErrorSeverity errorSeverity; + + @override + late ErrorType type; + + TestErrorCode(String name, String message) + : super( + problemMessage: message, + name: name, + uniqueName: 'TestErrorCode.$name', + ); +} diff --git a/pkg/linter/test/pubspec_test.dart b/pkg/linter/test/pubspec_test.dart new file mode 100644 index 00000000000..b3e465ff13b --- /dev/null +++ b/pkg/linter/test/pubspec_test.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import 'rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(BadPubspecTest); + }); +} + +@reflectiveTest +class BadPubspecTest extends LintRuleTest { + @override + String get lintRule => 'sort_pub_dependencies'; + + // ignore: non_constant_identifier_names + test_malformedPubspec() async { + await assertNoPubspecDiagnostics(r''' +not: a + valid + pub + spec: +'''); + } +} diff --git a/pkg/linter/test/rule_test.dart b/pkg/linter/test/rule_test.dart new file mode 100644 index 00000000000..468c445460b --- /dev/null +++ b/pkg/linter/test/rule_test.dart @@ -0,0 +1,329 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/analysis_options/analysis_options_provider.dart'; +import 'package:analyzer/src/analysis_options/apply_options.dart'; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/lint/io.dart'; +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/ast.dart'; +import 'package:linter/src/formatter.dart'; +import 'package:linter/src/rules.dart'; +import 'package:linter/src/rules/implementation_imports.dart'; +import 'package:linter/src/rules/package_prefixed_library_names.dart'; +import 'package:linter/src/test_utilities/annotation.dart'; +import 'package:linter/src/test_utilities/test_resource_provider.dart'; +import 'package:linter/src/utils.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'experiments_test.dart' as experiment_tests; +import 'test_constants.dart'; +import 'util/annotation_matcher.dart'; +import 'util/test_utils.dart'; + +void main() { + group('rule tests', () { + setUp(setUpSharedTestEnvironment); + defineRuleTests(); + experiment_tests.main(); + defineRuleUnitTests(); + }); +} + +/// Rule tests +void defineRuleTests() { + group('rule', () { + group('dart', () { + // Rule tests run with default analysis options. + testRules(ruleTestDataDir); + + // Rule tests run against specific configurations. + for (var entry in Directory(testConfigDir).listSync()) { + if (entry is! Directory) continue; + group('(config: ${p.basename(entry.path)})', () { + var analysisOptionsFile = + File(p.join(entry.path, 'analysis_options.yaml')); + var analysisOptions = analysisOptionsFile.readAsStringSync(); + testRules(ruleTestDataDir, analysisOptions: analysisOptions); + }); + } + }); + group('pub', () { + for (var entry in Directory(p.join(ruleTestDataDir, 'pub')).listSync()) { + if (entry is Directory) { + for (var child in entry.listSync()) { + if (child is File && isPubspecFile(child)) { + var ruleName = p.basename(entry.path); + testRule(ruleName, child); + } + } + } + } + }); + }); +} + +void defineRuleUnitTests() { + group('uris', () { + group('isPackage', () { + for (var uri in [ + Uri.parse('package:foo/src/bar.dart'), + Uri.parse('package:foo/src/baz/bar.dart') + ]) { + test(uri.toString(), () { + expect(isPackage(uri), isTrue); + }); + } + for (var uri in [ + Uri.parse('foo/bar.dart'), + Uri.parse('src/bar.dart'), + Uri.parse('dart:async') + ]) { + test(uri.toString(), () { + expect(isPackage(uri), isFalse); + }); + } + }); + + group('samePackage', () { + test('identity', () { + expect( + samePackage(Uri.parse('package:foo/src/bar.dart'), + Uri.parse('package:foo/src/bar.dart')), + isTrue); + }); + test('foo/bar.dart', () { + expect( + samePackage(Uri.parse('package:foo/src/bar.dart'), + Uri.parse('package:foo/bar.dart')), + isTrue); + }); + }); + + group('implementation', () { + for (var uri in [ + Uri.parse('package:foo/src/bar.dart'), + Uri.parse('package:foo/src/baz/bar.dart') + ]) { + test(uri.toString(), () { + expect(isImplementation(uri), isTrue); + }); + } + for (var uri in [ + Uri.parse('package:foo/bar.dart'), + Uri.parse('src/bar.dart') + ]) { + test(uri.toString(), () { + expect(isImplementation(uri), isFalse); + }); + } + }); + }); + + group('names', () { + group('keywords', () { + var good = ['class', 'if', 'assert', 'catch', 'import']; + testEach(good, isKeyWord, isTrue); + var bad = ['_class', 'iff', 'assert_', 'Catch']; + testEach(bad, isKeyWord, isFalse); + }); + group('identifiers', () { + var good = [ + 'foo', + '_if', + '_', + 'f2', + 'fooBar', + 'foo_bar', + '\$foo', + 'foo\$Bar', + 'foo\$' + ]; + testEach(good, isValidDartIdentifier, isTrue); + var bad = ['if', '42', '3', '2f']; + testEach(bad, isValidDartIdentifier, isFalse); + }); + group('library_name_prefixes', () { + bool isGoodPrefix(List v) => matchesOrIsPrefixedBy( + v[3], + Analyzer.facade.createLibraryNamePrefix( + libraryPath: v[0], projectRoot: v[1], packageName: v[2])); + + var good = [ + ['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.src.a'], + ['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'acme.a'], + ['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.test.a'], + ['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.data.a'], + ['/u/b/c/lib/acme.dart', '/u/b/c', 'acme', 'acme'] + ]; + testEach(good, isGoodPrefix, isTrue); + + var bad = [ + ['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.a'], + ['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'wrk.acme.a'], + ['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.a'], + ['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.a'] + ]; + testEach(bad, isGoodPrefix, isFalse); + }); + }); +} + +void testRule(String ruleName, File file, + {bool debug = true, + bool failOnErrors = true, + bool useMockSdk = true, + String? analysisOptions}) { + test(ruleName, () async { + if (!file.existsSync()) { + throw Exception('No rule found defined at: ${file.path}'); + } + + // Disable this check until migration is complete internally. + noSoundNullSafety = false; + + try { + var errorInfos = await _getErrorInfos(ruleName, file, + useMockSdk: useMockSdk, + debug: debug, + analysisOptions: analysisOptions); + _validateExpectedLints(file, errorInfos, + debug: debug, + failOnErrors: failOnErrors, + analysisOptions: analysisOptions); + } finally { + noSoundNullSafety = true; + } + }); +} + +void testRules(String ruleDir, {String? analysisOptions}) { + for (var entry in Directory(ruleDir).listSync()) { + if (entry is! File || !isDartFile(entry)) continue; + var ruleName = p.basenameWithoutExtension(entry.path); + testRule(ruleName, entry, analysisOptions: analysisOptions); + } +} + +Future> _getErrorInfos(String ruleName, File file, + {required bool useMockSdk, + required bool debug, + required String? analysisOptions}) async { + registerLintRules(inTestMode: debug); + var rule = Registry.ruleRegistry[ruleName]; + if (rule == null) { + fail('rule `$ruleName` is not registered; unable to test.'); + } + + if (useMockSdk) { + var driver = buildDriver(rule, file, analysisOptions: analysisOptions); + return await driver.lintFiles([file]); + } + + var path = p.normalize(file.absolute.path); + var collection = AnalysisContextCollection( + includedPaths: [path], + resourceProvider: PhysicalResourceProvider.INSTANCE, + ); + + var context = collection.contexts[0]; + var options = context.analysisOptions as AnalysisOptionsImpl; + options.lintRules = context.analysisOptions.lintRules.toList(); + options.lintRules.add(rule); + options.lint = true; + + var result = + await context.currentSession.getResolvedUnit(path) as ResolvedUnitResult; + return [ + AnalysisErrorInfoImpl( + result.errors, + result.lineInfo, + ) + ]; +} + +/// Parse lint annotations in the given [file] and validate that they correspond +/// with errors in the provided [errorInfos]. +void _validateExpectedLints(File file, Iterable errorInfos, + {bool debug = true, bool failOnErrors = true, String? analysisOptions}) { + var expected = []; + + var lineNumber = 1; + for (var line in file.readAsLinesSync()) { + var annotation = extractAnnotation(lineNumber, line); + if (annotation != null) { + expected.add(AnnotationMatcher(annotation)); + } + ++lineNumber; + } + + var actual = []; + var errors = []; + for (var info in errorInfos) { + for (var error in info.errors) { + var errorType = error.errorCode.type; + if (errorType == ErrorType.LINT) { + actual.add(Annotation.forError(error, info.lineInfo)); + } else if (failOnErrors && errorType.severity == ErrorSeverity.ERROR) { + var location = info.lineInfo.getLocation(error.offset); + errors.add( + '${file.path} ${location.lineNumber}:${location.columnNumber} ${error.message}'); + } + } + } + + if (errors.isNotEmpty) { + fail(['Unexpected diagnostics:\n', ...errors].join('\n')); + } + + actual.sort(); + try { + expect(actual, unorderedMatches(expected)); + // TODO (asashour): to be removed after fixing + // https://github.com/dart-lang/linter/issues/909 + // ignore: avoid_catches_without_on_clauses + } catch (_) { + if (debug) { + // Dump results for debugging purposes. + + // AST + var optionsProvider = AnalysisOptionsProvider(); + var optionMap = optionsProvider.getOptionsFromString(analysisOptions); + var optionsImpl = AnalysisOptionsImpl(); + optionsImpl.applyOptions(optionMap); + + var features = optionsImpl.contextFeatures; + + FileSpelunker(file.absolute.path, featureSet: features).spelunk(); + printToConsole(''); + // Lints. + ResultReporter(errorInfos).write(); + } + + // Rethrow and fail. + rethrow; + } +} + +/// A [LintFilter] that filters no lint. +class NoFilter implements LintFilter { + @override + bool filter(AnalysisError lint) => false; +} + +/// A [DetailedReporter] that filters no lint, only used in debug mode, when +/// actual lints do not match expectations. +class ResultReporter extends DetailedReporter { + ResultReporter(Iterable errors) + : super(errors, NoFilter(), stdout); +} diff --git a/pkg/linter/test/rule_test_support.dart b/pkg/linter/test/rule_test_support.dart new file mode 100644 index 00000000000..075e536c320 --- /dev/null +++ b/pkg/linter/test/rule_test_support.dart @@ -0,0 +1,664 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/src/dart/analysis/byte_store.dart'; +import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; +import 'package:analyzer/src/error/analyzer_error_code.dart'; +import 'package:analyzer/src/lint/pub.dart'; +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer/src/lint/util.dart'; +import 'package:analyzer/src/test_utilities/mock_packages.dart'; +import 'package:analyzer/src/test_utilities/mock_sdk.dart'; +import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart'; +import 'package:collection/collection.dart'; +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/rules.dart'; +import 'package:meta/meta.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'rule_test_support.dart'; + +export 'package:analyzer/src/dart/analysis/experiments.dart'; +export 'package:analyzer/src/dart/error/syntactic_errors.dart'; +export 'package:analyzer/src/error/codes.dart'; +export 'package:analyzer/src/test_utilities/package_config_file_builder.dart'; + +ExpectedError error(ErrorCode code, int offset, int length, + {Pattern? messageContains}) => + ExpectedError(code, offset, length, messageContains: messageContains); + +typedef DiagnosticMatcher = bool Function(AnalysisError error); + +class AnalysisOptionsFileConfig { + final List experiments; + final List lints; + final bool propagateLinterExceptions; + + AnalysisOptionsFileConfig({ + this.experiments = const [], + this.lints = const [], + this.propagateLinterExceptions = false, + }); + + String toContent() { + var buffer = StringBuffer(); + + if (experiments.isNotEmpty || propagateLinterExceptions) { + buffer.writeln('analyzer:'); + buffer.writeln(' enable-experiment:'); + for (var experiment in experiments) { + buffer.writeln(' - $experiment'); + } + + if (propagateLinterExceptions) { + buffer.writeln(' strong-mode:'); + buffer.writeln( + ' propagate-linter-exceptions: $propagateLinterExceptions', + ); + } + } + + buffer.writeln('linter:'); + buffer.writeln(' rules:'); + for (var lint in lints) { + buffer.writeln(' - $lint'); + } + + return buffer.toString(); + } +} + +/// A description of a diagnostic that is expected to be reported. +class ExpectedDiagnostic { + final DiagnosticMatcher diagnosticMatcher; + + /// The offset of the beginning of the diagnostic's region. + final int offset; + + /// The offset of the beginning of the diagnostic's region. + final int length; + + /// A pattern that should be contained in the diagnostic message or `null` if + /// the message contents should not be checked. + final Pattern? messageContains; + + /// Initialize a newly created diagnostic description. + ExpectedDiagnostic(this.diagnosticMatcher, this.offset, this.length, + {this.messageContains}); + + /// Return `true` if the [error] matches this description of what it's + /// expected to be. + bool matches(AnalysisError error) { + if (!diagnosticMatcher(error)) return false; + if (error.offset != offset) return false; + if (error.length != length) return false; + if (messageContains != null && !error.message.contains(messageContains!)) { + return false; + } + + return true; + } +} + +class ExpectedError extends ExpectedDiagnostic { + final ErrorCode code; + + /// Initialize a newly created error description. + ExpectedError(this.code, int offset, int length, {Pattern? messageContains}) + : super((AnalysisError error) => error.errorCode == code, offset, length, + messageContains: messageContains); +} + +class ExpectedLint extends ExpectedDiagnostic { + final String lintName; + + /// Initialize a newly created lint description. + ExpectedLint(this.lintName, int offset, int length, + {Pattern? messageContains}) + : super((AnalysisError error) => error.errorCode.name == lintName, offset, + length, + messageContains: messageContains); + + /// Initialize a newly created lint description. + ExpectedLint.withLintCode(LintCode lintCode, int offset, int length, + {Pattern? messageContains}) + : lintName = lintCode.uniqueName, + super((AnalysisError error) => error.errorCode == lintCode, offset, + length, + messageContains: messageContains); +} + +mixin LanguageVersion219Mixin on PubPackageResolutionTest { + @override + String? get testPackageLanguageVersion => '2.19'; +} + +abstract class LintRuleTest extends PubPackageResolutionTest { + String get lintRule; + + @override + List get _lintRules { + var ruleName = lintRule; + if (!Registry.ruleRegistry.any((r) => r.name == ruleName)) { + throw Exception("Unrecognized rule: '$ruleName'"); + } + return [ruleName]; + } + + ExpectedLint lint(int offset, int length, {Pattern? messageContains}) => + ExpectedLint(lintRule, offset, length, messageContains: messageContains); +} + +class PubPackageResolutionTest extends _ContextResolutionTest { + final List _lintRules = const []; + + bool get addFlutterPackageDep => false; + + bool get addJsPackageDep => false; + + bool get addMetaPackageDep => false; + + @override + List get collectionIncludedPaths => [workspaceRootPath]; + + bool get dumpAstOnFailures => true; + + List get experiments => []; + + /// The path that is not in [workspaceRootPath], contains external packages. + String get packagesRootPath => '/packages'; + + String get testFileName => 'test.dart'; + + @override + String get testFilePath => '$testPackageLibPath/$testFileName'; + + String? get testPackageLanguageVersion => null; + + String get testPackageLibPath => '$testPackageRootPath/lib'; + + String get testPackagePubspecPath => '$testPackageRootPath/pubspec.yaml'; + + String get testPackageRootPath => '$workspaceRootPath/test'; + + String get workspaceRootPath => '/home'; + + /// Assert that the number of diagnostics that have been gathered matches the + /// number of [expectedDiagnostics] and that they have the expected error + /// descriptions and locations. The order in which the diagnostics were + /// gathered is ignored. + Future assertDiagnostics( + String code, List expectedDiagnostics) async { + addTestFile(code); + await resolveTestFile(); + await assertDiagnosticsIn(errors, expectedDiagnostics); + } + + /// Assert that the diagnostics in [errors] match [expectedDiagnostics]. + Future assertDiagnosticsIn(List errors, + List expectedDiagnostics) async { + // + // Match actual diagnostics to expected diagnostics. + // + var unmatchedActual = errors.toList(); + var unmatchedExpected = expectedDiagnostics.toList(); + var actualIndex = 0; + while (actualIndex < unmatchedActual.length) { + var matchFound = false; + var expectedIndex = 0; + while (expectedIndex < unmatchedExpected.length) { + if (unmatchedExpected[expectedIndex] + .matches(unmatchedActual[actualIndex])) { + matchFound = true; + unmatchedActual.removeAt(actualIndex); + unmatchedExpected.removeAt(expectedIndex); + break; + } + expectedIndex++; + } + if (!matchFound) { + actualIndex++; + } + } + // + // Write the results. + // + var buffer = StringBuffer(); + if (unmatchedExpected.isNotEmpty) { + buffer.writeln('Expected but did not find:'); + for (var expected in unmatchedExpected) { + buffer.write(' '); + if (expected is ExpectedError) { + buffer.write(expected.code); + } + if (expected is ExpectedLint) { + buffer.write(expected.lintName); + } + buffer.write(' ['); + buffer.write(expected.offset); + buffer.write(', '); + buffer.write(expected.length); + buffer.writeln(']'); + } + } + if (unmatchedActual.isNotEmpty) { + if (buffer.isNotEmpty) { + buffer.writeln(); + } + buffer.writeln('Found but did not expect:'); + for (var actual in unmatchedActual) { + buffer.write(' '); + buffer.write(actual.errorCode); + buffer.write(' ['); + buffer.write(actual.offset); + buffer.write(', '); + buffer.write(actual.length); + buffer.write(', '); + buffer.write(actual.message); + buffer.writeln(']'); + } + } + if (buffer.isNotEmpty) { + errors.sort((first, second) => first.offset.compareTo(second.offset)); + buffer.writeln(); + buffer.writeln('To accept the current state, expect:'); + for (var actual in errors) { + late String diagnosticKind; + Object? description; + if (actual.errorCode is LintCode) { + diagnosticKind = 'lint'; + } else { + diagnosticKind = 'error'; + description = actual.errorCode; + } + buffer.write(' $diagnosticKind('); + if (description != null) { + buffer.write(description); + buffer.write(', '); + } + buffer.write(actual.offset); + buffer.write(', '); + buffer.write(actual.length); + buffer.writeln('),'); + } + + if (dumpAstOnFailures) { + buffer.writeln(); + buffer.writeln(); + try { + var astSink = CollectingSink(); + + StringSpelunker(result.unit.toSource(), + sink: astSink, featureSet: result.unit.featureSet) + .spelunk(); + buffer.write(astSink.buffer); + buffer.writeln(); + // I hereby choose to catch this type. + // ignore: avoid_catching_errors + } on ArgumentError catch (_) { + // Perhaps we encountered a parsing error while spelunking. + } + } + + fail(buffer.toString()); + } + } + + /// Assert that there are no diagnostics in the given [code]. + Future assertNoDiagnostics(String code) async => + assertDiagnostics(code, const []); + + /// Assert that there are no diagnostics in [errors]. + Future assertNoDiagnosticsIn(List errors) => + assertDiagnosticsIn(errors, const []); + + /// Assert that no diagnostics are reported when resolving [content]. + Future assertNoPubspecDiagnostics(String content) async { + newFile(testPackagePubspecPath, content); + var errors = await _resolvePubspecFile(content); + await assertDiagnosticsIn(errors, []); + } + + /// Assert that [expectedDiagnostics] are reported when resolving [content]. + Future assertPubspecDiagnostics( + String content, List expectedDiagnostics) async { + newFile(testPackagePubspecPath, content); + var errors = await _resolvePubspecFile(content); + await assertDiagnosticsIn(errors, expectedDiagnostics); + } + + @override + @mustCallSuper + void setUp() { + super.setUp(); + // Check for any needlessly enabled experiments. + for (var experiment in experiments) { + var feature = ExperimentStatus.knownFeatures[experiment]; + if (feature?.isEnabledByDefault ?? false) { + fail("The '$experiment' experiment is enabled by default, " + 'try removing it from `experiments`.'); + } + } + + writeTestPackageAnalysisOptionsFile( + AnalysisOptionsFileConfig( + experiments: experiments, + lints: _lintRules, + propagateLinterExceptions: true, + ), + ); + writeTestPackageConfig( + PackageConfigFileBuilder(), + ); + } + + void writePackageConfig(String path, PackageConfigFileBuilder config) { + newFile2( + path, + config.toContent( + toUriStr: toUriStr, + ), + ); + } + + void writeTestPackageAnalysisOptionsFile(AnalysisOptionsFileConfig config) { + newAnalysisOptionsYamlFile( + testPackageRootPath, + config.toContent(), + ); + } + + void writeTestPackageConfig(PackageConfigFileBuilder config) { + var configCopy = config.copy(); + + configCopy.add( + name: 'test', + rootPath: testPackageRootPath, + languageVersion: testPackageLanguageVersion, + ); + + if (addFlutterPackageDep) { + var flutterPath = '/packages/flutter'; + addFlutterPackageFiles( + getFolder(flutterPath), + ); + configCopy.add(name: 'flutter', rootPath: flutterPath); + } + + if (addJsPackageDep) { + var jsPath = '/packages/js'; + MockPackages.addJsPackageFiles( + getFolder(jsPath), + ); + configCopy.add(name: 'js', rootPath: jsPath); + } + + if (addMetaPackageDep) { + var metaPath = '/packages/meta'; + MockPackages.addMetaPackageFiles( + getFolder(metaPath), + ); + configCopy.add(name: 'meta', rootPath: metaPath); + } + + var path = '$testPackageRootPath/.dart_tool/package_config.json'; + writePackageConfig(path, configCopy); + } + + void writeTestPackagePubspecYamlFile(PubspecYamlFileConfig config) { + newPubspecYamlFile(testPackageRootPath, config.toContent()); + } + + Future> _resolvePubspecFile(String content) async { + var path = convertPath(testPackagePubspecPath); + var pubspecRules = >{}; + for (var rule in Registry.ruleRegistry + .where((rule) => _lintRules.contains(rule.name))) { + var visitor = rule.getPubspecVisitor(); + if (visitor != null) { + pubspecRules[rule] = visitor; + } + } + + if (pubspecRules.isEmpty) { + throw UnsupportedError( + 'Resolving pubspec files only supported with rules with ' + 'PubspecVisitors.'); + } + + var sourceUri = resourceProvider.pathContext.toUri(path); + var pubspecAst = Pubspec.parse(content, + sourceUrl: sourceUri, resourceProvider: resourceProvider); + var listener = RecordingErrorListener(); + var reporter = ErrorReporter( + listener, resourceProvider.getFile(path).createSource(sourceUri), + isNonNullableByDefault: false); + for (var entry in pubspecRules.entries) { + entry.key.reporter = reporter; + pubspecAst.accept(entry.value); + } + return [...listener.errors]; + } + + /// Create a fake 'flutter' package that can be used by tests. + static void addFlutterPackageFiles(Folder rootFolder) { + var libFolder = rootFolder.getChildAssumingFolder('lib'); + libFolder.getChildAssumingFile('widgets.dart').writeAsStringSync(r''' +export 'src/widgets/container.dart'; +export 'src/widgets/framework.dart'; +'''); + + libFolder + .getChildAssumingFolder('src') + .getChildAssumingFolder('widgets') + .getChildAssumingFile('container.dart') + .writeAsStringSync(r''' +import 'framework.dart'; + +// This is found in dart:ui. +class Color { + Color(int value); +} + +class Container extends StatelessWidget { + const Container({ + super.key, + Color? color, + double? width, + double? height, + Widget? child, + }); +} + +class SizedBox implements Widget {} + +class Row implements Widget {} +'''); + + libFolder + .getChildAssumingFolder('src') + .getChildAssumingFolder('widgets') + .getChildAssumingFile('framework.dart') + .writeAsStringSync(r''' +abstract class BuildContext { + Widget get widget; + bool get mounted; +} + +class Key { + Key(String value); +} + +class Navigator { + static NavigatorState of( + BuildContext context, {bool rootNavigator = false}) => NavigatorState(); +} + +class NavigatorState {} + +abstract class StatefulWidget extends Widget { + const StatefulWidget({super.key}); + + State createState(); +} + +class State {} + +abstract class Widget { + final Key? key; + + const Widget({thi.key}); +} + +abstract class StatelessWidget extends Widget { + const StatelessWidget({super.key}); + + @protected + Widget build(BuildContext context); +} +'''); + } +} + +class PubspecYamlFileConfig { + final String? name; + final String? sdkVersion; + final List dependencies; + + PubspecYamlFileConfig({ + this.name, + this.sdkVersion, + this.dependencies = const [], + }); + + String toContent() { + var buffer = StringBuffer(); + + if (name != null) { + buffer.writeln('name: $name'); + } + + if (sdkVersion != null) { + buffer.writeln('environment:'); + buffer.writeln(" sdk: '$sdkVersion'"); + } + + if (dependencies.isNotEmpty) { + buffer.writeln('dependencies:'); + for (var dependency in dependencies) { + buffer.writeln(' ${dependency.name}: ${dependency.version}'); + } + } + + return buffer.toString(); + } +} + +class PubspecYamlFileDependency { + final String name; + final String version; + + PubspecYamlFileDependency({ + required this.name, + this.version = 'any', + }); +} + +abstract class _ContextResolutionTest with ResourceProviderMixin { + static bool _lintRulesAreRegistered = false; + + final ByteStore _byteStore = MemoryByteStore(); + + AnalysisContextCollectionImpl? _analysisContextCollection; + + late ResolvedUnitResult result; + + List get collectionIncludedPaths; + + /// The analysis errors that were computed during analysis. + List get errors => result.errors + .whereNot((e) => ignoredErrorCodes.any((c) => e.errorCode == c)) + .toList(); + + /// Error codes that by default should be ignored in test expectations. + List get ignoredErrorCodes => + [WarningCode.UNUSED_LOCAL_VARIABLE]; + + Folder get sdkRoot => newFolder('/sdk'); + + String get testFilePath => '/test/lib/test.dart'; + + void addTestFile(String content) { + newFile2(testFilePath, content); + } + + @override + File newFile2(String path, String content) { + if (_analysisContextCollection != null && !path.endsWith('.dart')) { + throw StateError('Only dart files can be changed after analysis.'); + } + + return super.newFile(path, content); + } + + /// Resolves a Dart source file at [path]. + /// + /// [path] must be converted for this file system. + Future resolveFile(String path) async { + var analysisContext = _contextFor(path); + var session = analysisContext.currentSession; + return await session.getResolvedUnit(path) as ResolvedUnitResult; + } + + Future resolveTestFile() => _resolveFile(testFilePath); + + @mustCallSuper + void setUp() { + if (!_lintRulesAreRegistered) { + registerLintRules(); + _lintRulesAreRegistered = true; + } + + createMockSdk( + resourceProvider: resourceProvider, + root: sdkRoot, + ); + } + + DriverBasedAnalysisContext _contextFor(String path) { + _createAnalysisContexts(); + + var convertedPath = convertPath(path); + return _analysisContextCollection!.contextFor(convertedPath); + } + + /// Create all analysis contexts in [collectionIncludedPaths]. + void _createAnalysisContexts() { + if (_analysisContextCollection != null) { + return; + } + + _analysisContextCollection = AnalysisContextCollectionImpl( + byteStore: _byteStore, + declaredVariables: {}, + enableIndex: true, + includedPaths: collectionIncludedPaths.map(convertPath).toList(), + resourceProvider: resourceProvider, + sdkPath: sdkRoot.path, + ); + } + + /// Resolve the file with the [path] into [result]. + Future _resolveFile(String path) async { + var convertedPath = convertPath(path); + + result = await resolveFile(convertedPath); + } +} diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart new file mode 100644 index 00000000000..5f300bac070 --- /dev/null +++ b/pkg/linter/test/rules/all.dart @@ -0,0 +1,390 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'always_specify_types_test.dart' as always_specify_types; +import 'always_use_package_imports_test.dart' as always_use_package_imports; +import 'annotate_overrides_test.dart' as annotate_overrides; +import 'avoid_annotating_with_dynamic_test.dart' + as avoid_annotating_with_dynamic; +import 'avoid_equals_and_hash_code_on_mutable_classes_test.dart' + as avoid_equals_and_hash_code_on_mutable_classes; +import 'avoid_escaping_inner_quotes_test.dart' as avoid_escaping_inner_quotes; +import 'avoid_final_parameters_test.dart' as avoid_final_parameters; +import 'avoid_function_literals_in_foreach_calls_test.dart' + as avoid_function_literals_in_foreach_calls; +import 'avoid_init_to_null_test.dart' as avoid_init_to_null; +import 'avoid_positional_boolean_parameters_test.dart' + as avoid_positional_boolean_parameters; +import 'avoid_private_typedef_functions_test.dart' + as avoid_private_typedef_functions; +import 'avoid_redundant_argument_values_test.dart' + as avoid_redundant_argument_values; +import 'avoid_relative_lib_imports_test.dart' as avoid_relative_lib_imports; +import 'avoid_renaming_method_parameters_test.dart' + as avoid_renaming_method_parameters; +import 'avoid_return_types_on_setters_test.dart' + as avoid_return_types_on_setters; +import 'avoid_returning_this_test.dart' as avoid_returning_this; +import 'avoid_setters_without_getters_test.dart' + as avoid_setters_without_getters; +import 'avoid_shadowing_type_parameters_test.dart' + as avoid_shadowing_type_parameters; +import 'avoid_single_cascade_in_expression_statements_test.dart' + as avoid_single_cascade_in_expression_statements; +import 'avoid_types_as_parameter_names_test.dart' + as avoid_types_as_parameter_names; +import 'avoid_types_on_closure_parameters_test.dart' + as avoid_types_on_closure_parameters; +import 'avoid_unused_constructor_parameters_test.dart' + as avoid_unused_constructor_parameters; +import 'avoid_void_async_test.dart' as avoid_void_async; +import 'await_only_futures_test.dart' as await_only_futures; +import 'camel_case_extensions_test.dart' as camel_case_extensions; +import 'cancel_subscriptions_test.dart' as cancel_subscriptions; +import 'cast_nullable_to_non_nullable_test.dart' + as cast_nullable_to_non_nullable; +import 'collection_methods_unrelated_type_test.dart' + as collection_methods_unrelated_type; +import 'comment_references_test.dart' as comment_references; +import 'conditional_uri_does_not_exist_test.dart' + as conditional_uri_does_not_exist; +import 'constant_identifier_names_test.dart' as constant_identifier_names; +import 'dangling_library_doc_comments_test.dart' + as dangling_library_doc_comments; +import 'depend_on_referenced_packages_test.dart' + as depend_on_referenced_packages; +import 'deprecated_consistency_test.dart' as deprecated_consistency; +import 'deprecated_member_use_from_same_package_test.dart' + as deprecated_member_use_from_same_package; +import 'directives_ordering_test.dart' as directives_ordering; +import 'discarded_futures_test.dart' as discarded_futures; +import 'do_not_use_environment_test.dart' as do_not_use_environment; +import 'empty_catches_test.dart' as empty_catches; +import 'empty_constructor_bodies_test.dart' as empty_constructor_bodies; +import 'empty_statements_test.dart' as empty_statements; +import 'eol_at_end_of_file_test.dart' as eol_at_end_of_file; +import 'exhaustive_cases_test.dart' as exhaustive_cases; +import 'file_names_test.dart' as file_names; +import 'flutter_style_todos_test.dart' as flutter_style_todos; +import 'hash_and_equals_test.dart' as hash_and_equals; +import 'implicit_reopen_test.dart' as implicit_reopen; +import 'invalid_case_patterns_test.dart' as invalid_case_patterns; +import 'join_return_with_assignment_test.dart' as join_return_with_assignment; +import 'leading_newlines_in_multiline_strings_test.dart' + as leading_newlines_in_multiline_strings; +import 'library_annotations_test.dart' as library_annotations; +import 'library_names_test.dart' as library_names; +import 'library_private_types_in_public_api_test.dart' + as library_private_types_in_public_api; +import 'lines_longer_than_80_chars_test.dart' as lines_longer_than_80_chars; +import 'literal_only_boolean_expressions_test.dart' + as literal_only_boolean_expressions; +import 'matching_super_parameters_test.dart' as matching_super_parameters; +import 'missing_whitespace_between_adjacent_strings_test.dart' + as missing_whitespace_between_adjacent_strings; +import 'no_adjacent_strings_in_list_test.dart' as no_adjacent_strings_in_list; +import 'no_duplicate_case_values_test.dart' as no_duplicate_case_values; +import 'no_leading_underscores_for_local_identifiers_test.dart' + as no_leading_underscores_for_local_identifiers; +import 'no_logic_in_create_state_test.dart' as no_logic_in_create_state; +import 'no_self_assignments_test.dart' as no_self_assignments; +import 'no_wildcard_variable_uses_test.dart' as no_wildcard_variable_uses; +import 'non_constant_identifier_names_test.dart' + as non_constant_identifier_names; +import 'noop_primitive_operations_test.dart' as noop_primitive_operations; +import 'null_check_on_nullable_type_parameter_test.dart' + as null_check_on_nullable_type_parameter; +import 'null_closures_test.dart' as null_closures; +import 'omit_local_variable_types_test.dart' as omit_local_variable_types; +import 'one_member_abstracts_test.dart' as one_member_abstracts; +import 'only_throw_errors_test.dart' as only_throw_errors; +import 'overridden_fields_test.dart' as overridden_fields; +import 'parameter_assignments_test.dart' as parameter_assignments; +import 'prefer_adjacent_string_concatenation_test.dart' + as prefer_adjacent_string_concatenation; +import 'prefer_asserts_in_initializer_lists_test.dart' + as prefer_asserts_in_initializer_lists; +import 'prefer_collection_literals_test.dart' as prefer_collection_literals; +import 'prefer_const_constructors_in_immutables_test.dart' + as prefer_const_constructors_in_immutables; +import 'prefer_const_constructors_test.dart' as prefer_const_constructors; +import 'prefer_const_declarations_test.dart' as prefer_const_declarations; +import 'prefer_const_literals_to_create_immutables_test.dart' + as prefer_const_literals_to_create_immutables; +import 'prefer_constructors_over_static_methods_test.dart' + as prefer_constructors_over_static_methods; +import 'prefer_contains_test.dart' as prefer_contains; +import 'prefer_double_quotes_test.dart' as prefer_double_quotes; +import 'prefer_expression_function_bodies_test.dart' + as prefer_expression_function_bodies; +import 'prefer_final_fields_test.dart' as prefer_final_fields; +import 'prefer_final_in_for_each_test.dart' as prefer_final_in_for_each; +import 'prefer_final_locals_test.dart' as prefer_final_locals; +import 'prefer_final_parameters_test.dart' as prefer_final_parameters; +import 'prefer_for_elements_to_map_fromIterable_test.dart' +// ignore: library_prefixes + as prefer_for_elements_to_map_fromIterable; +import 'prefer_generic_function_type_aliases_test.dart' + as prefer_generic_function_type_aliases; +import 'prefer_if_elements_to_conditional_expressions_test.dart' + as prefer_if_elements_to_conditional_expressions; +import 'prefer_inlined_adds_test.dart' as prefer_inlined_adds; +import 'prefer_interpolation_to_compose_strings_test.dart' + as prefer_interpolation_to_compose_strings; +// ignore: library_prefixes +import 'prefer_iterable_whereType_test.dart' as prefer_iterable_whereType; +import 'prefer_mixin_test.dart' as prefer_mixin; +import 'prefer_null_aware_method_calls_test.dart' + as prefer_null_aware_method_calls; +import 'prefer_null_aware_operators_test.dart' as prefer_null_aware_operators; +import 'prefer_relative_imports_test.dart' as prefer_relative_imports; +import 'prefer_single_quotes_test.dart' as prefer_single_quotes; +import 'prefer_spread_collections_test.dart' as prefer_spread_collections; +import 'prefer_typing_uninitialized_variables_test.dart' + as prefer_typing_uninitialized_variables; +import 'prefer_void_to_null_test.dart' as prefer_void_to_null; +import 'provide_deprecation_message_test.dart' as provide_deprecation_message; +import 'public_member_api_docs_test.dart' as public_member_api_docs; +import 'recursive_getters_test.dart' as recursive_getters; +import 'secure_pubspec_urls_test.dart' as secure_pubspec_urls; +import 'sized_box_for_whitespace_test.dart' as sized_box_for_whitespace; +import 'slash_for_doc_comments_test.dart' as slash_for_doc_comments; +import 'sort_constructors_first_test.dart' as sort_constructors_first; +import 'sort_pub_dependencies_test.dart' as sort_pub_dependencies; +import 'sort_unnamed_constructors_first_test.dart' + as sort_unnamed_constructors_first; +import 'test_types_in_equals_test.dart' as test_types_in_equals; +import 'throw_in_finally_test.dart' as throw_in_finally; +import 'tighten_type_of_initializing_formals_test.dart' + as tighten_type_of_initializing_formals; +import 'type_annotate_public_apis_test.dart' as type_annotate_public_apis; +import 'type_init_formals_test.dart' as type_init_formals; +import 'type_literal_in_constant_pattern_test.dart' + as type_literal_in_constant_pattern; +import 'unawaited_futures_test.dart' as unawaited_futures; +import 'unnecessary_await_in_return_test.dart' as unnecessary_await_in_return; +import 'unnecessary_brace_in_string_interps_test.dart' + as unnecessary_brace_in_string_interps; +import 'unnecessary_breaks_test.dart' as unnecessary_breaks; +import 'unnecessary_const_test.dart' as unnecessary_const; +import 'unnecessary_constructor_name_test.dart' as unnecessary_constructor_name; +import 'unnecessary_final_test.dart' as unnecessary_final; +import 'unnecessary_getters_setters_test.dart' as unnecessary_getters_setters; +import 'unnecessary_lambdas_test.dart' as unnecessary_lambdas; +import 'unnecessary_late_test.dart' as unnecessary_late; +import 'unnecessary_library_directive_test.dart' + as unnecessary_library_directive; +import 'unnecessary_new_test.dart' as unnecessary_new; +import 'unnecessary_null_aware_assignments_test.dart' + as unnecessary_null_aware_assignments; +import 'unnecessary_null_aware_operator_on_extension_on_nullable_test.dart' + as unnecessary_null_aware_operator_on_extension_on_nullable; +import 'unnecessary_null_checks_test.dart' as unnecessary_null_checks; +import 'unnecessary_null_in_if_null_operators_test.dart' + as unnecessary_null_in_if_null_operators; +import 'unnecessary_nullable_for_final_variable_declarations_test.dart' + as unnecessary_nullable_for_final_variable_declarations; +import 'unnecessary_overrides_test.dart' as unnecessary_overrides; +import 'unnecessary_parenthesis_test.dart' as unnecessary_parenthesis; +import 'unnecessary_raw_strings_test.dart' as unnecessary_raw_strings; +import 'unnecessary_statements_test.dart' as unnecessary_statements; +import 'unnecessary_string_escapes_test.dart' as unnecessary_string_escapes; +import 'unnecessary_string_interpolations_test.dart' + as unnecessary_string_interpolations; +import 'unnecessary_this_test.dart' as unnecessary_this; +import 'unnecessary_to_list_in_spreads_test.dart' + as unnecessary_to_list_in_spreads; +import 'unreachable_from_main_test.dart' as unreachable_from_main; +import 'unrelated_type_equality_checks_test.dart' + as unrelated_type_equality_checks; +import 'use_build_context_synchronously_test.dart' + as use_build_context_synchronously; +import 'use_colored_box_test.dart' as use_colored_box; +import 'use_enums_test.dart' as use_enums; +import 'use_full_hex_values_for_flutter_colors_test.dart' + as use_full_hex_values_for_flutter_colors; +import 'use_function_type_syntax_for_parameters_test.dart' + as use_function_type_syntax_for_parameters; +import 'use_if_null_to_convert_nulls_to_bools_test.dart' + as use_if_null_to_convert_nulls_to_bools; +import 'use_is_even_rather_than_modulo_test.dart' + as use_is_even_rather_than_modulo; +import 'use_key_in_widget_constructors_test.dart' + as use_key_in_widget_constructors; +import 'use_late_for_private_fields_and_variables_test.dart' + as use_late_for_private_fields_and_variables; +import 'use_named_constants_test.dart' as use_named_constants; +import 'use_rethrow_when_possible_test.dart' as use_rethrow_when_possible; +import 'use_setters_to_change_properties_test.dart' + as use_setters_to_change_properties; +import 'use_string_buffers_test.dart' as use_string_buffers; +import 'use_super_parameters_test.dart' as use_super_parameters; +import 'use_test_throws_matchers_test.dart' as use_test_throws_matchers; +import 'use_to_and_as_if_applicable_test.dart' as use_to_and_as_if_applicable; +import 'valid_regexps_test.dart' as valid_regexps; +import 'void_checks_test.dart' as void_checks; + +void main() { + always_specify_types.main(); + always_use_package_imports.main(); + annotate_overrides.main(); + avoid_annotating_with_dynamic.main(); + avoid_equals_and_hash_code_on_mutable_classes.main(); + avoid_escaping_inner_quotes.main(); + avoid_final_parameters.main(); + avoid_function_literals_in_foreach_calls.main(); + avoid_init_to_null.main(); + avoid_positional_boolean_parameters.main(); + avoid_private_typedef_functions.main(); + avoid_redundant_argument_values.main(); + avoid_relative_lib_imports.main(); + avoid_renaming_method_parameters.main(); + avoid_return_types_on_setters.main(); + avoid_returning_this.main(); + avoid_setters_without_getters.main(); + avoid_shadowing_type_parameters.main(); + avoid_single_cascade_in_expression_statements.main(); + avoid_types_as_parameter_names.main(); + avoid_types_on_closure_parameters.main(); + avoid_unused_constructor_parameters.main(); + avoid_void_async.main(); + await_only_futures.main(); + camel_case_extensions.main(); + cancel_subscriptions.main(); + cast_nullable_to_non_nullable.main(); + collection_methods_unrelated_type.main(); + comment_references.main(); + conditional_uri_does_not_exist.main(); + constant_identifier_names.main(); + dangling_library_doc_comments.main(); + depend_on_referenced_packages.main(); + deprecated_consistency.main(); + deprecated_member_use_from_same_package.main(); + directives_ordering.main(); + discarded_futures.main(); + do_not_use_environment.main(); + empty_catches.main(); + empty_constructor_bodies.main(); + empty_statements.main(); + eol_at_end_of_file.main(); + exhaustive_cases.main(); + file_names.main(); + flutter_style_todos.main(); + hash_and_equals.main(); + implicit_reopen.main(); + invalid_case_patterns.main(); + join_return_with_assignment.main(); + leading_newlines_in_multiline_strings.main(); + library_annotations.main(); + library_names.main(); + library_private_types_in_public_api.main(); + lines_longer_than_80_chars.main(); + literal_only_boolean_expressions.main(); + matching_super_parameters.main(); + missing_whitespace_between_adjacent_strings.main(); + no_adjacent_strings_in_list.main(); + no_duplicate_case_values.main(); + no_leading_underscores_for_local_identifiers.main(); + no_logic_in_create_state.main(); + no_self_assignments.main(); + no_wildcard_variable_uses.main(); + non_constant_identifier_names.main(); + noop_primitive_operations.main(); + null_check_on_nullable_type_parameter.main(); + null_closures.main(); + omit_local_variable_types.main(); + one_member_abstracts.main(); + only_throw_errors.main(); + overridden_fields.main(); + parameter_assignments.main(); + prefer_adjacent_string_concatenation.main(); + prefer_asserts_in_initializer_lists.main(); + prefer_collection_literals.main(); + prefer_const_constructors_in_immutables.main(); + prefer_const_constructors.main(); + prefer_const_declarations.main(); + prefer_const_literals_to_create_immutables.main(); + prefer_constructors_over_static_methods.main(); + prefer_contains.main(); + prefer_double_quotes.main(); + prefer_expression_function_bodies.main(); + prefer_final_fields.main(); + prefer_final_in_for_each.main(); + prefer_final_locals.main(); + prefer_final_parameters.main(); + prefer_for_elements_to_map_fromIterable.main(); + prefer_generic_function_type_aliases.main(); + prefer_if_elements_to_conditional_expressions.main(); + prefer_inlined_adds.main(); + prefer_interpolation_to_compose_strings.main(); + prefer_iterable_whereType.main(); + prefer_mixin.main(); + prefer_null_aware_method_calls.main(); + prefer_null_aware_operators.main(); + prefer_relative_imports.main(); + prefer_single_quotes.main(); + prefer_spread_collections.main(); + prefer_typing_uninitialized_variables.main(); + prefer_void_to_null.main(); + provide_deprecation_message.main(); + public_member_api_docs.main(); + recursive_getters.main(); + secure_pubspec_urls.main(); + sized_box_for_whitespace.main(); + slash_for_doc_comments.main(); + sort_constructors_first.main(); + sort_pub_dependencies.main(); + sort_unnamed_constructors_first.main(); + test_types_in_equals.main(); + throw_in_finally.main(); + tighten_type_of_initializing_formals.main(); + type_annotate_public_apis.main(); + type_init_formals.main(); + type_literal_in_constant_pattern.main(); + unawaited_futures.main(); + unnecessary_await_in_return.main(); + unnecessary_brace_in_string_interps.main(); + unnecessary_breaks.main(); + unnecessary_const.main(); + unnecessary_constructor_name.main(); + unnecessary_final.main(); + unnecessary_getters_setters.main(); + unnecessary_lambdas.main(); + unnecessary_late.main(); + unnecessary_library_directive.main(); + unnecessary_new.main(); + unnecessary_null_aware_assignments.main(); + unnecessary_null_aware_operator_on_extension_on_nullable.main(); + unnecessary_null_checks.main(); + unnecessary_null_in_if_null_operators.main(); + unnecessary_nullable_for_final_variable_declarations.main(); + unnecessary_overrides.main(); + unnecessary_parenthesis.main(); + unnecessary_raw_strings.main(); + unnecessary_statements.main(); + unnecessary_string_escapes.main(); + unnecessary_string_interpolations.main(); + unnecessary_this.main(); + unnecessary_to_list_in_spreads.main(); + unreachable_from_main.main(); + unrelated_type_equality_checks.main(); + use_build_context_synchronously.main(); + use_colored_box.main(); + use_enums.main(); + use_full_hex_values_for_flutter_colors.main(); + use_function_type_syntax_for_parameters.main(); + use_if_null_to_convert_nulls_to_bools.main(); + use_is_even_rather_than_modulo.main(); + use_key_in_widget_constructors.main(); + use_late_for_private_fields_and_variables.main(); + use_named_constants.main(); + use_rethrow_when_possible.main(); + use_setters_to_change_properties.main(); + use_string_buffers.main(); + use_super_parameters.main(); + use_test_throws_matchers.main(); + use_to_and_as_if_applicable.main(); + valid_regexps.main(); + void_checks.main(); +} diff --git a/pkg/linter/test/rules/always_specify_types_test.dart b/pkg/linter/test/rules/always_specify_types_test.dart new file mode 100644 index 00000000000..9a59b9d2382 --- /dev/null +++ b/pkg/linter/test/rules/always_specify_types_test.dart @@ -0,0 +1,137 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AlwaysSpecifyTypesTest); + }); +} + +@reflectiveTest +class AlwaysSpecifyTypesTest extends LintRuleTest { + @override + String get lintRule => 'always_specify_types'; + + test_listPattern_destructured() async { + await assertDiagnostics(r''' +f() { + var [a] = [1]; +} +''', [ + lint(13, 1), + ]); + } + + test_listPattern_destructured_listLiteral() async { + await assertDiagnostics(r''' +f() { + var [int a] = [1]; +} +''', [ + lint(22, 1), + ]); + } + + test_listPattern_destructured_ok() async { + await assertNoDiagnostics(r''' +f() { + var [int a] = [1]; +} +'''); + } + + test_mapPattern_destructured() async { + await assertDiagnostics(r''' +f() { + var {'a': a} = {'a': 1}; +} +''', [ + lint(18, 1), + ]); + } + + test_mapPattern_destructured_ok() async { + await assertNoDiagnostics(r''' +f() { + var {'a': int a} = {'a': 1}; +} +'''); + } + + test_objectPattern_switch_final() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && final b): + } +} +''', [ + lint(79, 5), + ]); + } + + test_objectPattern_switch_ok() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && int b): + } +} +'''); + } + + test_objectPattern_switch_var() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && var b): + } +} +''', [ + lint(79, 3), + ]); + } + + test_recordPattern_switch() async { + await assertDiagnostics(r''' +f() { + switch ((1, 2)) { + case (final a, var b): + } +} +''', [ + lint(36, 5), + lint(45, 3), + ]); + } + + test_recordPattern_switch_ok() async { + await assertNoDiagnostics(r''' +f() { + switch ((1, 2)) { + case (int a, int b): + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/always_use_package_imports_test.dart b/pkg/linter/test/rules/always_use_package_imports_test.dart new file mode 100644 index 00000000000..7d51e468b80 --- /dev/null +++ b/pkg/linter/test/rules/always_use_package_imports_test.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AlwaysUsePackageImportsTest); + }); +} + +@reflectiveTest +class AlwaysUsePackageImportsTest extends LintRuleTest { + @override + String get lintRule => 'always_use_package_imports'; + + test_samePackage_packageSchema() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + await assertNoDiagnostics(r''' +/// This provides [C]. +import 'package:test/lib.dart'; +'''); + } + + test_samePackage_packageSchema_fromOutsideLib() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + var bin = newFile('$testPackageRootPath/bin/bin.dart', r''' +/// This provides [C]. +import 'package:test/lib.dart'; +'''); + await resolveTestFile(); + var result = await resolveFile(bin.path); + await assertNoDiagnosticsIn(result.errors); + } + + test_samePackage_relativeUri() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + await assertDiagnostics(r''' +/// This provides [C]. +import 'lib.dart'; +''', [ + lint(30, 10), + ]); + } + + test_internalPackage() async { + var packageConfigBuilder = PackageConfigFileBuilder(); + packageConfigBuilder.add( + name: 'internal_package', + rootPath: '$testPackageRootPath/vendor/internal_package', + ); + writeTestPackageConfig(packageConfigBuilder); + + newFile('$testPackageRootPath/vendor/internal_package/lib/lib.dart', r''' +class C {} +'''); + await assertNoDiagnostics(r''' +/// This provides [C]. +import 'package:internal_package/lib.dart'; +'''); + } +} diff --git a/pkg/linter/test/rules/analysis_options.yaml b/pkg/linter/test/rules/analysis_options.yaml new file mode 100644 index 00000000000..f6cc9166442 --- /dev/null +++ b/pkg/linter/test/rules/analysis_options.yaml @@ -0,0 +1,6 @@ +include: ../../analysis_options.yaml + +linter: + rules: + # For reflective test naming + non_constant_identifier_names: false diff --git a/pkg/linter/test/rules/annotate_overrides_test.dart b/pkg/linter/test/rules/annotate_overrides_test.dart new file mode 100644 index 00000000000..81ff5cb8fb7 --- /dev/null +++ b/pkg/linter/test/rules/annotate_overrides_test.dart @@ -0,0 +1,159 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AnnotateOverridesTest); + }); +} + +@reflectiveTest +class AnnotateOverridesTest extends LintRuleTest { + @override + String get lintRule => 'annotate_overrides'; + + test_class_fieldWithAnnotation() async { + await assertNoDiagnostics(r''' +class A { + int get x => 4; +} + +class B extends A { + @override + int x = 5; +} +'''); + } + + // TODO(srawlins): Test subclassing via `implements`, via mixing-in, and via + // superconstraints. + // Test that extension methods don't need an annotation. + // Test setters and operators. + + test_class_fieldWithoutAnnotation() async { + await assertDiagnostics(r''' +class A { + int get x => 4; +} + +class B extends A { + int x = 5; +} +''', [ + lint(57, 1), + ]); + } + + test_class_getterWithAnnotation() async { + await assertNoDiagnostics(r''' +class A { + int get x => 4; +} + +class B extends A { + @override + int get x => 5; +} +'''); + } + + test_class_getterWithoutAnnotation() async { + await assertDiagnostics(r''' +class A { + int get x => 4; +} + +class B extends A { + int get x => 5; +} +''', [ + lint(61, 1), + ]); + } + + test_class_methodWithAnnotation() async { + await assertNoDiagnostics(r''' +class A { + void f() {} +} + +class B extends A { + @override + f() {} +} +'''); + } + + test_class_methodWithoutAnnotation() async { + await assertDiagnostics(r''' +class A { + void f() {} +} + +class B extends A { + void f() {} +} +''', [ + lint(54, 1), + ]); + } + + test_enum_fieldWithAnnotation() async { + await assertNoDiagnostics(r''' +class O { + int get x => 0; +} + +enum A implements O { + a,b,c; + @override + int get x => 0; +} +'''); + } + + test_enum_fieldWithoutAnnotation() async { + await assertDiagnostics(r''' +class O { + int get x => 0; +} + +enum A implements O { + a,b,c; + int get x => 0; +} +''', [ + lint(72, 1), + ]); + } + + test_enum_methodWithAnnotation() async { + await assertNoDiagnostics(r''' +class O { + int m() => 0; +} + +enum A implements O { + a,b,c; + @override + int m() => 0; +} +'''); + } + + test_enum_methodWithoutAnnotation() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + String toString() => ''; +} +''', [ + lint(27, 8), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_annotating_with_dynamic_test.dart b/pkg/linter/test/rules/avoid_annotating_with_dynamic_test.dart new file mode 100644 index 00000000000..0a793815703 --- /dev/null +++ b/pkg/linter/test/rules/avoid_annotating_with_dynamic_test.dart @@ -0,0 +1,89 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidAnnotatingWithDynamicTest); + }); +} + +@reflectiveTest +class AvoidAnnotatingWithDynamicTest extends LintRuleTest { + @override + String get lintRule => 'avoid_annotating_with_dynamic'; + + // TODO(srawlins): Test parameter of function-typed typedef (both old and + // new style). + // Test parameter of function-typed parameter (`f(void g(dynamic x))`). + // Test parameter with a default value. + + test_fieldFormals() async { + await assertDiagnostics(r''' +class A { + var a; + A(dynamic this.a); +} +''', [ + lint(23, 14), + ]); + } + + test_returnType() async { + await assertNoDiagnostics(r''' +dynamic f() { + return null; +} +'''); + } + + test_requiredParameter() async { + await assertDiagnostics(r''' +void f(dynamic p) {} +''', [ + lint(7, 9), + ]); + } + + test_implicitDynamic() async { + await assertNoDiagnostics(r''' +void f(p) {} +'''); + } + + test_optionalParameter() async { + await assertDiagnostics(r''' +void f([dynamic p]) {} +''', [ + lint(8, 9), + ]); + } + + test_optionalNamedParameter() async { + await assertDiagnostics(r''' +void f({dynamic p}) {} +''', [ + lint(8, 9), + ]); + } + + test_super() async { + await assertDiagnostics(r''' +class A { + var a; + var b; + A(this.a, this.b); +} +class B extends A { + B(dynamic super.a, dynamic super.b); +} +''', [ + lint(75, 15), + lint(92, 15), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_equals_and_hash_code_on_mutable_classes_test.dart b/pkg/linter/test/rules/avoid_equals_and_hash_code_on_mutable_classes_test.dart new file mode 100644 index 00000000000..ad58450b326 --- /dev/null +++ b/pkg/linter/test/rules/avoid_equals_and_hash_code_on_mutable_classes_test.dart @@ -0,0 +1,91 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidEqualsAndHashCodeOnMutableClassesTest); + }); +} + +@reflectiveTest +class AvoidEqualsAndHashCodeOnMutableClassesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + @override + String get lintRule => 'avoid_equals_and_hash_code_on_mutable_classes'; + + test_enums() async { + await assertDiagnostics(r''' +enum E { + e(1), f(2), g(3); + final int key; + const E(this.key); + bool operator ==(Object other) => other is E && other.key == key; + int get hashCode => key.hashCode; +} +''', [ + error( + CompileTimeErrorCode.ILLEGAL_CONCRETE_ENUM_MEMBER_DECLARATION, 83, 2), + error(CompileTimeErrorCode.ILLEGAL_CONCRETE_ENUM_MEMBER_DECLARATION, 145, + 8), + // No lint. + ]); + } + + test_immutableClass() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class A { + final String key; + const A(this.key); + @override + operator ==(other) => other is A && other.key == key; + @override + int get hashCode => key.hashCode; +} +'''); + } + + test_mutableClass() async { + await assertDiagnostics(r''' +class A { + final String key; + const A(this.key); + @override + operator ==(other) => other is A && other.key == key; + @override + int get hashCode => key.hashCode; +} +''', [ + lint(65, 8), + lint(133, 3), + ]); + } + + test_subtypeOfImmutableClass() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class A { + const A(); +} + +class B extends A { + final String key; + const B(this.key); + @override + operator ==(other) => other is B && other.key == key; + @override + int get hashCode => key.hashCode; +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_escaping_inner_quotes_test.dart b/pkg/linter/test/rules/avoid_escaping_inner_quotes_test.dart new file mode 100644 index 00000000000..49a8068ccdb --- /dev/null +++ b/pkg/linter/test/rules/avoid_escaping_inner_quotes_test.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidEscapingInnerQuotesTest); + }); +} + +@reflectiveTest +class AvoidEscapingInnerQuotesTest extends LintRuleTest { + @override + String get lintRule => 'avoid_escaping_inner_quotes'; + + test_doubleQuotes_empty() async { + await assertNoDiagnostics(r''' +void f() { + print(""); +} +'''); + } + + test_doubleQuotes_escapedQuote() async { + await assertDiagnostics(r''' +void f() { + print("\""); +} +''', [ + lint(19, 4), + ]); + } + + test_doubleQuotes_escapedQuote_withInterpolation() async { + await assertDiagnostics(r''' +void f() { + print("\"$f"); +} +''', [ + lint(19, 6), + ]); + } + + test_doubleQuotes_escapedQuote_withSingleQuote() async { + await assertNoDiagnostics(r''' +void f() { + print("\"'"); +} +'''); + } + + test_doubleQuotes_escapedQuote_withSingleQuote_andInterpolation() async { + await assertNoDiagnostics(r''' +void f() { + print("\"'$f"); +} +'''); + } + + test_singleQuotes() async { + await assertDiagnostics(r''' +void f(String d) { + print('a\'b\'c ${d.length}'); +} +''', [ + lint(27, 21), + ]); + } + + test_singleQuotes_empty() async { + await assertNoDiagnostics(r''' +void f() { + print(''); +} +'''); + } + + test_singleQuotes_escapedQuote() async { + await assertDiagnostics(r''' +void f() { + print('\''); +} +''', [ + lint(19, 4), + ]); + } + + test_singleQuotes_escapedQuote_withDoubleQuote() async { + await assertNoDiagnostics(r''' +void f() { + print('\'"'); +} +'''); + } + + test_singleQuotes_escapedQuote_withDoubleQuote_andInterpolation() async { + await assertNoDiagnostics(r''' +void f() { + print('\'"$f'); +} +'''); + } + + test_singleQuotes_escapedQuote_withInterpolation() async { + await assertDiagnostics(r''' +void f() { + print('\'$f'); +} +''', [ + lint(19, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_final_parameters_test.dart b/pkg/linter/test/rules/avoid_final_parameters_test.dart new file mode 100644 index 00000000000..db5b2fe0a0a --- /dev/null +++ b/pkg/linter/test/rules/avoid_final_parameters_test.dart @@ -0,0 +1,198 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidFinalParametersTest); + }); +} + +@reflectiveTest +class AvoidFinalParametersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_final_parameters'; + + // TODO(srawlins): Test function-typed parameter like `void f(final p())`. + + test_constructorFieldFormal_final() async { + await assertDiagnostics(r''' +class C { + int p; + C(final this.p); +} +''', [ + // TODO(srawlins): Do not report this lint rule here, as it is redundant + // with the Warning. + error(WarningCode.UNNECESSARY_FINAL, 23, 5), + lint(23, 12), + ]); + } + + test_constructorFieldFormal_noFinal() async { + await assertNoDiagnostics(r''' +class C { + int p; + C(this.p); +} +'''); + } + + test_constructorSimple_final() async { + await assertDiagnostics(r''' +class C { + C(final int p); +} +''', [ + lint(14, 11), + ]); + } + + test_constructorSimple_noFinal() async { + await assertNoDiagnostics(r''' +class C { + C(int p); +} +'''); + } + + test_functionExpression_final() async { + await assertDiagnostics(r''' +var f = (final int value) {}; +''', [ + lint(9, 15), + ]); + } + + test_functionExpression_noFinal() async { + await assertNoDiagnostics(r''' +var f = (int value) {}; +'''); + } + + test_operator_final() async { + await assertDiagnostics(r''' +class C { + int operator +(final int other) => 0; +} +''', [ + lint(27, 15), + ]); + } + + test_operator_noFinal() async { + await assertNoDiagnostics(r''' +class C { + int operator +(int other) => 0; +} +'''); + } + + test_optionalNamed_final() async { + await assertDiagnostics(r''' +void f({final int? p}) {} +''', [ + lint(8, 12), + ]); + } + + test_optionalNamed_noFinal() async { + await assertNoDiagnostics(r''' +void f({int? p}) {} +'''); + } + + test_optionalPositional_final() async { + await assertDiagnostics(r''' +void f([final int? p]) {} +''', [ + lint(8, 12), + ]); + } + + test_optionalPositional_noFinal() async { + await assertNoDiagnostics(r''' +void f([int? p]) {} +'''); + } + + test_optionalPositionalWithDefault_final() async { + await assertDiagnostics(r''' +void f([final int p = 0]) {} +''', [ + lint(8, 15), + ]); + } + + test_optionalPositionalWithDefault_noFinal() async { + await assertNoDiagnostics(r''' +void f([int p = 0]) {} +'''); + } + + test_requiredNamed_final() async { + await assertDiagnostics(r''' +void f({required final int? p}) {} +''', [ + lint(8, 21), + ]); + } + + test_requiredNamed_noFinal() async { + await assertNoDiagnostics(r''' +void f({required int p}) {} +'''); + } + + test_requiredPositional_final() async { + await assertDiagnostics(r''' +void f(final int p) {} +''', [ + lint(7, 11), + ]); + } + + test_requiredPositional_noFinal() async { + await assertNoDiagnostics(r''' +void f(int p) {} +'''); + } + + test_setter_final() async { + await assertDiagnostics(r''' +set f(final int value) {} +''', [ + lint(6, 15), + ]); + } + + test_setter_noFinal() async { + await assertNoDiagnostics(r''' +set f(int value) {} +'''); + } + + test_super() async { + await assertDiagnostics(r''' +class A { + String? a; + String? b; + A(this.a, this.b); +} +class B extends A { + B(final super.a, final super.b); +} +''', [ + // TODO(srawlins): Do not report this lint rule here, as it is redundant + // with the Hint. + error(WarningCode.UNNECESSARY_FINAL, 83, 5), + error(WarningCode.UNNECESSARY_FINAL, 98, 5), + lint(83, 13), + lint(98, 13), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_function_literals_in_foreach_calls_test.dart b/pkg/linter/test/rules/avoid_function_literals_in_foreach_calls_test.dart new file mode 100644 index 00000000000..f6326127491 --- /dev/null +++ b/pkg/linter/test/rules/avoid_function_literals_in_foreach_calls_test.dart @@ -0,0 +1,180 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidFunctionLiteralsInForeachCalls); + defineReflectiveTests(AvoidFunctionLiteralsInForeachCallsPreNNBDTest); + }); +} + +@reflectiveTest +class AvoidFunctionLiteralsInForeachCalls extends LintRuleTest { + @override + String get lintRule => 'avoid_function_literals_in_foreach_calls'; + + test_expectedIdentifier() async { + await assertDiagnostics(r''' +void f(dynamic iter) => iter?.forEach(...); +''', [ + // No lint + error(ParserErrorCode.MISSING_IDENTIFIER, 38, 3), + ]); + } + + test_functionExpression_nullableTarget() async { + await assertNoDiagnostics(r''' +void f(List? people) { + people?.forEach((person) => print('$person!')); +} +'''); + } + + test_functionExpression_nullAwareChain() async { + await assertNoDiagnostics(r''' +void f(Map? people) { + people?.keys.forEach((person) => print('$person!')); +} +'''); + } + + test_functionExpression_targetDoesNotHaveMethodChain() async { + await assertDiagnostics(r''' +void f(List> people) { + people + .first + .forEach((person) => print('$person!')); +} +''', [ + lint(65, 7), + ]); + } + + test_functionExpression_targetHasMethodChain() async { + await assertNoDiagnostics(r''' +void f(List people) { + people + .map((person) => person.toUpperCase()) + .forEach((person) => print('$person!')); +} +'''); + } + + test_functionExpression_targetHasCascade() async { + await assertNoDiagnostics(r''' +void f(List>> lists) { + final lists2 = lists..first.forEach((list) { + list.add('y'); + }); +} +'''); + } + + test_functionExpression_targetHasCascadeAndNullAssert() async { + await assertNoDiagnostics(r''' +class C { + List? items; +} + +void f(C obj) { + final result = obj..items!.forEach((s) { + print(s); + }); +} +'''); + } + + test_functionExpression_targetInIrrelevantNestedCascade() async { + await assertDiagnostics(r''' +void f(List>> lists) { + final lists2 = lists..forEach((list) { + list.forEach((item) { + print(item); + }); + }); +} +''', [ + lint(91, 7), + ]); + } + + test_functionExpressionWithBlockBody() async { + await assertDiagnostics(r''' +void f(List people) { + people.forEach((person) { + print('$person!'); + }); +} +''', [ + lint(39, 7), + ]); + } + + test_functionExpressionWithExpressionBody() async { + await assertDiagnostics(r''' +void f(List people) { + people.forEach((person) => print('$person!')); +} +''', [ + lint(39, 7), + ]); + } + + test_nonFunctionExpression() async { + await assertNoDiagnostics(r''' +void f(List people) { + people.forEach(print); +} +'''); + } + + test_nonFunctionExpression_targetHasMethodChain() async { + await assertNoDiagnostics(r''' +void f(List people) { + people + .map((person) => person.toUpperCase()) + .forEach(print); +} +'''); + } +} + +@reflectiveTest +class AvoidFunctionLiteralsInForeachCallsPreNNBDTest extends LintRuleTest { + @override + String get lintRule => 'avoid_function_literals_in_foreach_calls'; + + @override + setUp() { + super.setUp(); + noSoundNullSafety = false; + } + + tearDown() { + noSoundNullSafety = true; + } + + test_functionExpression_basic() async { + await assertDiagnostics(r''' +// @dart=2.9 +void f(List people) { + people.forEach((person) => print('$person')); +} +''', [lint(52, 7)]); + } + + test_functionExpression_nullAware() async { + await assertNoDiagnostics(r''' +// @dart=2.9 +void f(List people) { + people?.forEach((person) => print('$person')); +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_init_to_null_test.dart b/pkg/linter/test/rules/avoid_init_to_null_test.dart new file mode 100644 index 00000000000..e864c4b75c4 --- /dev/null +++ b/pkg/linter/test/rules/avoid_init_to_null_test.dart @@ -0,0 +1,262 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidInitToNullTest); + defineReflectiveTests(AvoidInitToNullSuperFormalsTest); + }); +} + +@reflectiveTest +class AvoidInitToNullSuperFormalsTest extends LintRuleTest { + @override + String get lintRule => 'avoid_init_to_null'; + + test_nullableStringInit() async { + await assertDiagnostics(r''' +class A { + String? a; + A({this.a = null}); +} +''', [ + lint(28, 13), + ]); + } + + test_superInit_2() async { + await assertDiagnostics(r''' +class A { + String? a; + A({this.a = null}); +} +class B extends A { + B({super.a = null}); +} +''', [ + lint(28, 13), + lint(72, 14), + ]); + } + + /// https://github.com/dart-lang/linter/issues/3349 + test_superInit_nolint() async { + await assertNoDiagnostics(r''' +class A { + String? a; + A({this.a = ''}); +} + +class B extends A { + B({super.a = null}); +} +'''); + } +} + +@reflectiveTest +class AvoidInitToNullTest extends LintRuleTest { + // todo (pq): mock and add FutureOr examples + + @override + String get lintRule => 'avoid_init_to_null'; + + test_fieldFormalParameter_inferredType() async { + await assertDiagnostics(r''' +class C { + int? i; + C({this.i = null}); +} +''', [ + lint(25, 13), + ]); + } + + test_instanceField_inferredType_final() async { + await assertNoDiagnostics(r''' +class C { + final i = null; +} +'''); + } + + test_instanceField_intType_noInitializer() async { + await assertNoDiagnostics(r''' +class C { + int i; + C(): i = 1; +} +'''); + } + + test_instanceField_nullableIntType() async { + await assertDiagnostics(r''' +class C { + int? i = null; + C(): i = 1; +} +''', [ + lint(17, 8), + ]); + } + + test_invalidAssignment_field() async { + await assertDiagnostics(r''' +class X { + int x = null; +} +''', [ + // No lint + error(CompileTimeErrorCode.INVALID_ASSIGNMENT, 20, 4), + ]); + } + + test_invalidAssignment_namedParameter() async { + await assertDiagnostics(r''' +class X { + X({int a = null}); +} +''', [ + // No lint + error(CompileTimeErrorCode.INVALID_ASSIGNMENT, 23, 4), + ]); + } + + test_invalidAssignment_namedParameter_fieldFormal() async { + await assertDiagnostics(r''' +class X { + int x; + X({this.x = null}); +} +''', [ + // No lint + error(CompileTimeErrorCode.INVALID_ASSIGNMENT, 33, 4), + ]); + } + + test_invalidAssignment_topLevelVariable() async { + await assertDiagnostics(r''' +int i = null; +''', [ + // No lint + error(CompileTimeErrorCode.INVALID_ASSIGNMENT, 8, 4), + ]); + } + + test_namedParameter_inferredType() async { + await assertDiagnostics(r''' +foo({p = null}) {} +''', [ + lint(5, 8), + ]); + } + + test_namedParameter_inferredType_defaultValueIsInt() async { + await assertNoDiagnostics(r''' +foo({p = 1}) {} +'''); + } + + test_namedParameter_inferredType_noDefaultValue() async { + await assertNoDiagnostics(r''' +foo({p}) {} +'''); + } + + test_namedParameter_inferredType_var() async { + await assertDiagnostics(r''' +foo({var p = null}) {} +''', [ + lint(5, 12), + ]); + } + + test_optionalParameter_inferredType() async { + await assertDiagnostics(r''' +foo([p = null]) {} +''', [ + lint(5, 8), + ]); + } + + test_optionalParameter_inferredType_defaultValueIsInt() async { + await assertNoDiagnostics(r''' +foo([p = 1]) {} +'''); + } + + test_optionalParameter_inferredType_noDefaultValue() async { + await assertNoDiagnostics(r''' +foo([p]) {} +'''); + } + + test_optionalParameter_inferredType_var() async { + await assertDiagnostics(r''' +foo([var p = null]) {} +''', [ + lint(5, 12), + ]); + } + + test_staticConstField_inferredType_final() async { + await assertNoDiagnostics(r''' +class C { + static const i = null; +} +'''); + } + + test_topLevelVariable_dynamic() async { + await assertDiagnostics(r''' +dynamic i = null; +''', [ + lint(8, 8), + ]); + } + + test_topLevelVariable_inferredType() async { + await assertDiagnostics(r''' +var i = null; +''', [ + lint(4, 8), + ]); + } + + test_topLevelVariable_inferredType_const() async { + await assertNoDiagnostics(r''' +const i = null; +'''); + } + + test_topLevelVariable_inferredType_final() async { + await assertNoDiagnostics(r''' +final i = null; +'''); + } + + test_topLevelVariable_inferredType_initializeToInt() async { + await assertNoDiagnostics(r''' +var i = 1; +'''); + } + + test_topLevelVariable_inferredType_noInitialization() async { + await assertNoDiagnostics(r''' +var i; +'''); + } + + test_topLevelVariable_nullableType() async { + await assertDiagnostics(r''' +int? ii = null; +''', [ + lint(5, 9), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_positional_boolean_parameters_test.dart b/pkg/linter/test/rules/avoid_positional_boolean_parameters_test.dart new file mode 100644 index 00000000000..c79db1fd72e --- /dev/null +++ b/pkg/linter/test/rules/avoid_positional_boolean_parameters_test.dart @@ -0,0 +1,248 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidPositionalBooleanParametersTest); + }); +} + +@reflectiveTest +class AvoidPositionalBooleanParametersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_positional_boolean_parameters'; + + test_anonymousFunction() async { + await assertNoDiagnostics(r''' +void f(List list) { + list.where((bool e) => e); +} +'''); + } + + test_constructor_fieldFormalParameter_named() async { + await assertNoDiagnostics(r''' +class C { + bool p; + C.named({this.p = false}); +} +'''); + } + + test_constructor_fieldFormalParameter_postional() async { + await assertDiagnostics(r''' +class C { + bool p; + C.named(this.p); +} +''', [ + lint(30, 6), + ]); + } + + test_constructor_named_withDefault() async { + await assertNoDiagnostics(r''' +class C { + C.named({bool p = false}) { + } +} +'''); + } + + test_constructor_positional() async { + await assertDiagnostics(r''' +class C { + C.named(bool a) { + } +} +''', [ + lint(20, 6), + ]); + } + + test_constructorPrivate_positionalOptional() async { + await assertNoDiagnostics(r''' +class C { + // ignore: unused_element_parameter + C._named([bool p = false]); +} +'''); + } + + test_extensionMethod() async { + await assertDiagnostics(r''' +extension Ext on int { + void f([bool p = false]) {} +} +''', [ + lint(33, 14), + ]); + } + + test_extensionMethod_unnamed() async { + await assertDiagnostics(r''' +extension on int { + // ignore: unused_element, unused_element_parameter + void f([bool p = false]) {} +} +''', [ + lint(83, 14), + ]); + } + + test_instanceMethod_named() async { + await assertNoDiagnostics(r''' +class C { + void m({bool p = true}) {} +} +'''); + } + + test_instanceMethod_overrideExtends_positionalOptional() async { + // TODO(srawlins): Test where the parameter in the override is _not found_ + // in the parent interface. + // TODO(srawlins): Test where the parameter is renamed. + await assertDiagnostics(r''' +class C { + void m([bool p = false]) {} +} +class D extends C { + @override + void m([bool p = false]) {} +} +''', [ + lint(20, 14), + ]); + } + + test_instanceMethod_overrideImplements_positionalOptional() async { + await assertDiagnostics(r''' +class C { + void m([bool p = false]) {} +} +abstract class D implements C { + @override + void m([bool p = false]) {} +} +''', [ + lint(20, 14), + ]); + } + + test_instanceMethod_positional() async { + await assertDiagnostics(r''' +class A { + void m(bool p) {} +} +''', [ + lint(19, 6), + ]); + } + + test_instanceMethod_positionalOptional() async { + await assertDiagnostics(r''' +class C { + void m([bool p = false]) {} +} +''', [ + lint(20, 14), + ]); + } + + test_instanceSetter() async { + await assertNoDiagnostics(r''' +class C { + set m(bool p) {} +} +'''); + } + + test_operator_indexAssignment() async { + await assertNoDiagnostics(r''' +class C { + void operator []=(int index, bool value) {} +} +'''); + } + + test_operator_minus() async { + await assertNoDiagnostics(r''' +class C { + void operator -(bool value) {} +} +'''); + } + + test_operator_plus() async { + await assertNoDiagnostics(r''' +class C { + void operator +(bool value) { + } +} +'''); + } + + test_staticMethod_namedWithDefault() async { + await assertNoDiagnostics(r''' +class B { + static void m({bool p = false}) {} +} +'''); + } + + test_staticMethod_positional() async { + await assertDiagnostics(r''' +class B { + static void m(bool p) {} +} +''', [ + lint(26, 6), + ]); + } + + test_topLevel_namedParameter() async { + await assertNoDiagnostics(r''' +void f({bool p = false}) {} +'''); + } + + test_topLevel_namedParameter_defaultValue() async { + await assertNoDiagnostics(r''' +void f({bool p = false}) {} +'''); + } + + test_topLevel_positionalParameter() async { + await assertDiagnostics(r''' +void f(bool p) {} +''', [ + lint(7, 6), + ]); + } + + test_topLevelPrivate() async { + await assertNoDiagnostics(r''' +// ignore: unused_element +void _f(bool p) {} +'''); + } + + test_typedef_named() async { + await assertNoDiagnostics(r''' +typedef T = Function({bool p}); +'''); + } + + test_typedef_positional() async { + await assertDiagnostics(r''' +typedef T = Function(bool p); +''', [ + lint(21, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_private_typedef_functions_test.dart b/pkg/linter/test/rules/avoid_private_typedef_functions_test.dart new file mode 100644 index 00000000000..71a7e12489b --- /dev/null +++ b/pkg/linter/test/rules/avoid_private_typedef_functions_test.dart @@ -0,0 +1,137 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidPrivateTypedefFunctionsTest); + }); +} + +@reflectiveTest +class AvoidPrivateTypedefFunctionsTest extends LintRuleTest { + @override + String get lintRule => 'avoid_private_typedef_functions'; + + test_nonFunctionTypeAlias() async { + await assertNoDiagnostics(r''' +// ignore: unused_element +typedef _Td = List; +'''); + } + + /// https://github.com/dart-lang/linter/issues/4665 + test_nonFunctionTypeAlias_record() async { + await assertNoDiagnostics(r''' +// ignore: unused_element +typedef _Record = (int a, int b); +'''); + } + + test_private_genericFunctionTypeAlias_usedMultipleTimes() async { + await assertNoDiagnostics(r''' +typedef _Td = int Function(); +late _Td td1; +late _Td td2; +'''); + } + + test_private_genericFunctionTypeAlias_usedMultipleTimes_declaredInPart() async { + var lib = newFile('$testPackageLibPath/lib.dart', r''' +part 'test.dart'; +typedef _Td = void Function(); +'''); + addTestFile(r''' +part of 'lib.dart'; +late _Td td1; +late _Td td2; +'''); + await resolveTestFile(); + var partResult = await resolveFile(lib.path); + await assertNoDiagnosticsIn(partResult.errors); + } + + test_private_genericFunctionTypeAlias_usedMultipleTimes_usedInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; +late _Td td1; +late _Td td2; +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; +typedef _Td = void Function(); +'''); + } + + test_private_genericFunctionTypeAlias_usedOneTime() async { + await assertDiagnostics(r''' +typedef _Td = int Function(); +late _Td td; +''', [ + lint(8, 3), + ]); + } + + test_private_genericFunctionTypeAlias_usedOneTime_declaredInPart() async { + var lib = newFile('$testPackageLibPath/lib.dart', r''' +part 'test.dart'; +typedef _Td = void Function(); +'''); + addTestFile(r''' +part of 'lib.dart'; +late _Td td; +'''); + await resolveTestFile(); + var partResult = await resolveFile(lib.path); + await assertDiagnosticsIn(partResult.errors, [ + lint(26, 3), + ]); + } + + test_private_genericFunctionTypeAlias_usedOneTime_usedInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; +late _Td td; +'''); + await assertDiagnostics(r''' +part 'part.dart'; +typedef _Td = void Function(); +''', [ + lint(26, 3), + ]); + } + + test_private_genericFunctionTypeAlias_usedZeroTimes() async { + await assertDiagnostics(r''' +// ignore: unused_element +typedef _Td = int Function(); +''', [ + lint(34, 3), + ]); + } + + test_private_legacyTypeAlias_usedZeroTimes() async { + await assertDiagnostics(r''' +// ignore: unused_element +typedef int _Td(); +''', [ + lint(38, 3), + ]); + } + + test_public_genericFunctionTypeAlias_usedZeroTimes() async { + await assertNoDiagnostics(r''' +typedef Td = int Function(); +'''); + } + + test_public_legacyTypeAlias_usedZeroTimes() async { + await assertNoDiagnostics(r''' +typedef int Td(); +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_redundant_argument_values_test.dart b/pkg/linter/test/rules/avoid_redundant_argument_values_test.dart new file mode 100644 index 00000000000..7a099114905 --- /dev/null +++ b/pkg/linter/test/rules/avoid_redundant_argument_values_test.dart @@ -0,0 +1,309 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidRedundantArgumentValuesTest); + defineReflectiveTests(AvoidRedundantArgumentValuesNamedArgsAnywhereTest); + }); +} + +@reflectiveTest +class AvoidRedundantArgumentValuesNamedArgsAnywhereTest extends LintRuleTest { + @override + String get lintRule => 'avoid_redundant_argument_values'; + + test_namedArgumentBeforePositional() async { + await assertDiagnostics(r''' +void foo(int a, int b, {bool c = true}) {} + +void f() { + foo(0, c: true, 1); +} +''', [ + lint(67, 4), + ]); + } +} + +@reflectiveTest +class AvoidRedundantArgumentValuesTest extends LintRuleTest { + @override + String get lintRule => 'avoid_redundant_argument_values'; + + @override + setUp() { + super.setUp(); + noSoundNullSafety = false; + } + + tearDown() { + noSoundNullSafety = true; + } + + /// https://github.com/dart-lang/linter/issues/3617 + test_enumDeclaration() async { + await assertDiagnostics(r''' +enum TestEnum { + a(test: false); + + const TestEnum({this.test = false}); + + final bool test; +} +''', [ + lint(26, 5), + ]); + } + + @FailingTest(issue: 'https://github.com/dart-lang/linter/issues/3447') + test_fromEnvironment() async { + await assertNoDiagnostics(r''' +const bool someDefine = bool.fromEnvironment('someDefine'); + +void f({bool test = true}) {} + +void g() { + f( + test: !someDefine, + ); +} +'''); + } + + /// https://github.com/dart-lang/sdk/issues/49596 + test_legacyRequired() async { + var a = newFile('$testPackageLibPath/a.dart', r''' +class Foo { + int? foo; + Foo({required this.foo}); +} +'''); + await resolveFile(a.path); + + await assertNoDiagnostics(r''' +// @dart = 2.9 +import 'a.dart'; + +void f() { + Foo(foo: null); +} +'''); + } + + test_redirectingFactoryConstructor() async { + await assertNoDiagnostics(r''' +class A { + factory A([int? value]) = B; + A._(); +} +class B extends A { + B([int? value = 2]) : super._(); +} +void f() { + A(); + A(null); + A(1); +} +'''); + } + + test_redirectingFactoryConstructor_multipleOptional() async { + await assertNoDiagnostics(r''' +class A { + factory A([int? one, int? two]) = B; + A._(); +} +class B extends A { + int? one; + int? two; + B([this.one = 2, this.two = 2]) : super._(); +} +void f() { + A(); + A(null, null); + A(1, 1); +} +'''); + } + + test_redirectingFactoryConstructor_named() async { + await assertNoDiagnostics(r''' +class A { + factory A({int? value}) = B; + A._(); +} +class B extends A { + B({int? value = 2}) : super._(); +} +void f() { + A(); + A(value: null); + A(value: 1); +} +'''); + } + + test_redirectingFactoryConstructor_named_redundant() async { + await assertDiagnostics(r''' +class A { + factory A({int? value}) = B; + A._(); +} +class B extends A { + B({int? value = 2}) : super._(); +} +void f() { + A(value: 2); +} +''', [ + lint(124, 8), + ]); + } + + test_redirectingFactoryConstructor_namedArgumentsAnywhere() async { + await assertNoDiagnostics(r''' +class A { + factory A(int? one, int? two, {int? three}) = B; + A._(); +} +class B extends A { + B(int? one, int? two, {int? three = 3}) : super._(); +} +void f() { + A(1, 2); + A(1, three: null, 2); + A(1, 2, three: null); + A(1, three: 4, 2); + A(three: 4, 1, 2); +} +'''); + } + + test_redirectingFactoryConstructor_namedArgumentsAnywhere_redundant() async { + await assertDiagnostics(r''' +class A { + factory A(int? one, int? two, {int? three}) = B; + A._(); +} +class B extends A { + B(int? one, int? two, {int? three = 3}) : super._(); +} +void f() { + A(1, three: 3, 2); +} +''', [ + lint(167, 8), + ]); + } + + test_redirectingFactoryConstructor_nested() async { + await assertNoDiagnostics(r''' +class A { + factory A([num? value]) = B; + A._(); +} +class B extends A { + factory B([num? value]) = C; + B._() : super._(); +} +class C extends B { + num? value; + C([this.value = 2]) : super._(); + + @override + String toString() => '$value'; +} +void f() { + A(); + A(null); + A(1); +} +'''); + } + + test_redirectingFactoryConstructor_redundant() async { + await assertDiagnostics(r''' +class A { + factory A([int? value]) = B; + A._(); +} +class B extends A { + B([int? value = 2]) : super._(); +} +void f() { + A(2); +} +''', [ + lint(124, 1), + ]); + } + + test_redirectingGenerativeConstructor() async { + await assertNoDiagnostics(r''' +class A { + A([int? value]) : this._(value); + A._([int? value = 2]); +} +void f() { + A(2); +} +'''); + } + + test_redirectingGenerativeConstructor_named() async { + await assertNoDiagnostics(r''' +class A { + A({int? value}) : this._(value: value); + A._({int? value = 2}); +} +void f() { + A(value: 2); +} +'''); + } + + test_redirectingGenerativeConstructor_named_redundant() async { + await assertDiagnostics(r''' +class A { + A({int? value}) : this._(value: value); + A._({int? value = 2}); +} +void f() { + A(value: null); +} +''', [ + lint(101, 4), + ]); + } + + test_redirectingGenerativeConstructor_redundant() async { + await assertDiagnostics(r''' +class A { + A([int? value]) : this._(value); + A._([int? value = 2]); +} +void f() { + A(null); +} +''', [ + lint(87, 4), + ]); + } + + test_requiredNullable() async { + await assertNoDiagnostics(r''' +void f({required int? x}) { } + +void main() { + f(x: null); +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_relative_lib_imports_test.dart b/pkg/linter/test/rules/avoid_relative_lib_imports_test.dart new file mode 100644 index 00000000000..1e3fa8a1259 --- /dev/null +++ b/pkg/linter/test/rules/avoid_relative_lib_imports_test.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidRelativeLibImportsTest); + }); +} + +@reflectiveTest +class AvoidRelativeLibImportsTest extends LintRuleTest { + @override + bool get addJsPackageDep => true; + + @override + String get lintRule => 'avoid_relative_lib_imports'; + + test_externalPackage() async { + await assertNoDiagnostics(r''' +/// This provides [JS]. +import 'package:js/js.dart'; +'''); + } + + test_samePackage_relativeUri() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + var test = newFile('$testPackageRootPath/test/test.dart', r''' +/// This provides [C]. +import '../lib/lib.dart'; +'''); + var lib2Result = await resolveFile(test.path); + await assertDiagnosticsIn(lib2Result.errors, [ + lint(30, 17), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_renaming_method_parameters_test.dart b/pkg/linter/test/rules/avoid_renaming_method_parameters_test.dart new file mode 100644 index 00000000000..fe1824d99e5 --- /dev/null +++ b/pkg/linter/test/rules/avoid_renaming_method_parameters_test.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + // TODO(srawlins): Add more tests: + // * indirect extension (C extends B extends A), + // * implementation (B implements A), + // * mixing in + // * mix-in applications + // * renaming with `_` + // * renaming with `__` (like `m(_, __)`) + defineReflectiveTests(AvoidRenamingMethodParametersTest); + }); +} + +@reflectiveTest +class AvoidRenamingMethodParametersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_renaming_method_parameters'; + + test_enumMixingIn() async { + await assertDiagnostics(r''' +mixin class C { + int f(int f) => f; +} +enum A with C { + a,b,c; + @override + int f(int x) => x; +} +''', [ + lint(88, 1), + ]); + } + + test_optionalPositional_renamed() async { + await assertDiagnostics(r''' +class A { + void m([int p = 0]) {} +} +class B extends A { + void m([int q = 0]) {} +} +''', [ + lint(71, 1), + ]); + } + + test_positional_docComments() async { + await assertNoDiagnostics(r''' +class A { + void m(int p) {} +} +class B extends A { + /// New comment. + void m(int q) {} +} +'''); + } + + test_positional_privateOverride() async { + await assertNoDiagnostics(r''' +class A { + void m(int p) {} +} +// ignore: unused_element +class _B extends A { + void m(int q) {} +} +'''); + } + + test_positional_renamed() async { + await assertDiagnostics(r''' +class A { + void m(int p) {} +} +class B extends A { + void m(int q) {} +} +''', [ + lint(64, 1), + ]); + } + + test_positional_renamed_nonLibSource() async { + newFile('$testPackageLibPath/a.dart', r''' +class A { + void m(int p) {} +} +'''); + var lib = newFile('$testPackageRootPath/test/a.dart', r''' +import '../lib/a.dart'; +class B extends A { + void m(int q) {} +} +'''); + var result = await resolveFile(lib.path); + await assertNoDiagnosticsIn(result.errors); + } + + test_positional_sameName() async { + await assertNoDiagnostics(r''' +class A { + void m(int p) {} +} +class B extends A { + void m(int p) {} +} +'''); + } + + test_zeroParameters() async { + await assertNoDiagnostics(r''' +class A { + void m() {} +} +class B extends A { + void m() {} +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_return_types_on_setters_test.dart b/pkg/linter/test/rules/avoid_return_types_on_setters_test.dart new file mode 100644 index 00000000000..8b591ec8615 --- /dev/null +++ b/pkg/linter/test/rules/avoid_return_types_on_setters_test.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidReturnTypesOnSettersTest); + }); +} + +@reflectiveTest +class AvoidReturnTypesOnSettersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_return_types_on_setters'; + + test_implicitReturnType() async { + await assertNoDiagnostics(r''' +set f(int p) {} +'''); + } + + test_instanceSetter_implicitReturnType() async { + await assertNoDiagnostics(r''' +class C { + set f(int p) {} +} +'''); + } + + test_instanceSetter_voidReturnType() async { + await assertDiagnostics(r''' +class C { + void set f(int p) {} +} +''', [ + lint(12, 4), + ]); + } + + test_staticSetter_implicitReturnType() async { + await assertNoDiagnostics(r''' +class C { + static set f(String p) {} +} +'''); + } + + test_staticSetter_voidReturnType() async { + await assertDiagnostics(r''' +class C { + static void set f(String p) {} +} +''', [ + lint(19, 4), + ]); + } + + test_voidReturnType() async { + await assertDiagnostics(r''' +void set f(int p) {} +''', [ + lint(0, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_returning_this_test.dart b/pkg/linter/test/rules/avoid_returning_this_test.dart new file mode 100644 index 00000000000..7ee1ddccbad --- /dev/null +++ b/pkg/linter/test/rules/avoid_returning_this_test.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidReturningThisTest); + }); +} + +@reflectiveTest +class AvoidReturningThisTest extends LintRuleTest { + @override + String get lintRule => 'avoid_returning_this'; + + /// https://github.com/dart-lang/linter/issues/3853 + test_conditionalReturn() async { + await assertNoDiagnostics(r''' +class C { + C getInstance(C? c) { + if (c == null) return this; + return c; + } +} +'''); + } + + test_conditionalReturn_expression_ternary() async { + await assertNoDiagnostics(r''' +class C { + C getInstance(C? c) => c == null ? this : c; +} +'''); + } + + test_conditionalReturn_ternary() async { + await assertNoDiagnostics(r''' +class C { + C getInstance(C? c) { + return c == null ? this : c; + } +} +'''); + } + + test_method() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + A aa() => this; +} +''', [ + lint(22, 2), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_setters_without_getters_test.dart b/pkg/linter/test/rules/avoid_setters_without_getters_test.dart new file mode 100644 index 00000000000..16c5d66bb55 --- /dev/null +++ b/pkg/linter/test/rules/avoid_setters_without_getters_test.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidSettersWithoutGettersTest); + }); +} + +@reflectiveTest +class AvoidSettersWithoutGettersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_setters_without_getters'; + + test_enum() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + set x(int x) {} +} +''', [ + lint(24, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_shadowing_type_parameters_test.dart b/pkg/linter/test/rules/avoid_shadowing_type_parameters_test.dart new file mode 100644 index 00000000000..7cf3a51f9c7 --- /dev/null +++ b/pkg/linter/test/rules/avoid_shadowing_type_parameters_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidShadowingTypeParametersTest); + }); +} + +@reflectiveTest +class AvoidShadowingTypeParametersTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'avoid_shadowing_type_parameters'; + + test_enum() async { + await assertDiagnostics(r''' +enum E { + a, b, c; + void fn() {} +} +''', [ + lint(33, 1), + ]); + } + + test_extensionType() async { + await assertDiagnostics(r''' +extension type E(int i) { + void m() {} +} +''', [ + lint(38, 1), + ]); + } + + test_wrongNumberOfTypeArguments() async { + await assertDiagnostics(r''' +typedef Predicate = bool (E element); +''', [ + // No lint. + error(CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS, 20, 8), + error(CompileTimeErrorCode.NON_TYPE_AS_TYPE_ARGUMENT, 26, 1), + error(ParserErrorCode.EXPECTED_TOKEN, 28, 1), + error(CompileTimeErrorCode.UNDEFINED_CLASS, 29, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_single_cascade_in_expression_statements_test.dart b/pkg/linter/test/rules/avoid_single_cascade_in_expression_statements_test.dart new file mode 100644 index 00000000000..855af09f3fb --- /dev/null +++ b/pkg/linter/test/rules/avoid_single_cascade_in_expression_statements_test.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidSingleCascadeInExpressionStatementsTest); + }); +} + +@reflectiveTest +class AvoidSingleCascadeInExpressionStatementsTest extends LintRuleTest { + @override + String get lintRule => 'avoid_single_cascade_in_expression_statements'; + + test_multipleCascades() async { + await assertNoDiagnostics(r''' +void f(int p) { + p..toString()..toString(); +} +'''); + } + + test_singleCascade() async { + await assertDiagnostics(r''' +void f(int p) { + p..toString(); +} +''', [ + lint(18, 13), + ]); + } + + test_singleCascade_asArgument() async { + await assertNoDiagnostics(r''' +void f(int p) { + g(p..toString()); +} + +void g(int p) {} +'''); + } + + test_singleCascade_inIfCondition() async { + await assertNoDiagnostics(r''' +void f(bool p) { + if (p..hashCode) {} +} +'''); + } + + test_singleCascade_thrown() async { + await assertNoDiagnostics(r''' +void f(int p) { + throw p..toString(); +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_types_as_parameter_names_test.dart b/pkg/linter/test/rules/avoid_types_as_parameter_names_test.dart new file mode 100644 index 00000000000..331db9070c5 --- /dev/null +++ b/pkg/linter/test/rules/avoid_types_as_parameter_names_test.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidTypesAsParameterNamesTest); + }); +} + +@reflectiveTest +class AvoidTypesAsParameterNamesTest extends LintRuleTest { + @override + String get lintRule => 'avoid_types_as_parameter_names'; + + test_super() async { + await assertDiagnostics(r''' +class A { + String a; + A(this.a); +} +class B extends A { + B(super.String); +} +''', [ + lint(67, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_types_on_closure_parameters_test.dart b/pkg/linter/test/rules/avoid_types_on_closure_parameters_test.dart new file mode 100644 index 00000000000..5f0f586f11b --- /dev/null +++ b/pkg/linter/test/rules/avoid_types_on_closure_parameters_test.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidTypesOnClosureParametersTest); + }); +} + +@reflectiveTest +class AvoidTypesOnClosureParametersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_types_on_closure_parameters'; + + test_argument() async { + await assertNoDiagnostics(r''' +void f(List list) { + list.map((e) => e.isEven); +} +'''); + } + + test_argument_typedParameter() async { + await assertDiagnostics(r''' +void f(List list) { + list.map((int e) => e.isEven); +} +''', [ + lint(37, 3), + ]); + } + + test_initializerInDeclaration_namedOptional() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertNoDiagnostics(r''' +var f = ({e}) => e.isEven; +'''); + } + + test_initializerInDeclaration_optionalNullable() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertNoDiagnostics(r''' +var f = ([e]) => e.name; +'''); + } + + test_initializerInDeclaration_optionalWithDefault() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertNoDiagnostics(r''' +var f = ({e = ''}) => e; +'''); + } + + test_initializerInDeclaration_parameterIsTyped_dynamic() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertNoDiagnostics(r''' +var goodName5 = (dynamic person) => person.name; +'''); + } + + test_initializerInDeclaration_parameterIsTyped_functionType() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertDiagnostics(r''' +var functionWithFunction = (int f(int x)) => f(0); +''', [ + lint(28, 12), + ]); + } + + test_initializerInDeclaration_parameterIsTyped_namedRequired() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertDiagnostics(r''' +var f = ({required int e}) => e.isEven; +''', [ + lint(19, 3), + ]); + } + + test_initializerInDeclaration_parameterIsTyped_optionalNullable() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertDiagnostics(r''' +var f = ([int? e]) => e?.isEven; +''', [ + lint(10, 4), + ]); + } + + test_initializerInDeclaration_parameterIsTyped_optionalWithDefault() async { + // TODO(srawlins): Any test which just assigns a function literal to a + // variable should be converted to something like an argument, or let + // downwards inference work in some other way. + // See https://github.com/dart-lang/linter/issues/1099. + await assertDiagnostics(r''' +var f = ([String e = '']) => e; +''', [ + lint(10, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/avoid_unused_constructor_parameters_test.dart b/pkg/linter/test/rules/avoid_unused_constructor_parameters_test.dart new file mode 100644 index 00000000000..3c21da3d3ae --- /dev/null +++ b/pkg/linter/test/rules/avoid_unused_constructor_parameters_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnusedConstructorParametersTest); + }); +} + +@reflectiveTest +class AvoidUnusedConstructorParametersTest extends LintRuleTest { + @override + String get lintRule => 'avoid_unused_constructor_parameters'; + + test_super() async { + await assertNoDiagnostics(r''' +class A { + String a; + String b; + A(this.a, this.b); +} +class B extends A { + B(super.a, super.b); +} +'''); + } +} diff --git a/pkg/linter/test/rules/avoid_void_async_test.dart b/pkg/linter/test/rules/avoid_void_async_test.dart new file mode 100644 index 00000000000..0cb978ef056 --- /dev/null +++ b/pkg/linter/test/rules/avoid_void_async_test.dart @@ -0,0 +1,280 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidVoidAsyncTest); + }); +} + +@reflectiveTest +class AvoidVoidAsyncTest extends LintRuleTest { + @override + String get lintRule => 'avoid_void_async'; + + test_asyncStar() async { + await assertNoDiagnostics(r''' +Stream f() async* {} +'''); + } + + test_futureVoidReturnType_arrow() async { + await assertNoDiagnostics(r''' +Future f() async => null; +'''); + } + + test_futureVoidReturnType_block() async { + await assertNoDiagnostics(r''' +Future c() async {} +'''); + } + + test_getter() async { + await assertNoDiagnostics(r''' +void get f => null; +'''); + } + + test_getter_async_voidReturnType() async { + await assertDiagnostics(r''' +void get f async => null; +''', [ + lint(9, 1), + ]); + } + + test_implicitReturnType_arrow() async { + await assertNoDiagnostics(r''' +void f() { + () async => null; +} +'''); + } + + test_implicitReturnType_block() async { + await assertNoDiagnostics(r''' +void f() { + () async {}; +} +'''); + } + + test_instanceGetter() async { + await assertDiagnostics(r''' +class Foo { + void get l async => null; +} +''', [ + lint(23, 1), + ]); + } + + test_instanceGetter_notAsync() async { + await assertNoDiagnostics(r''' +class Foo { + void get k => null; +} +'''); + } + + test_instanceMethod_arrow() async { + await assertDiagnostics(r''' +class Foo { + void d() async => null; +} +''', [ + lint(19, 1), + ]); + } + + test_instanceMethod_asyncStar() async { + await assertNoDiagnostics(r''' +class Foo { + Stream f() async* {} +} +'''); + } + + test_instanceMethod_block() async { + await assertDiagnostics(r''' +class Foo { + void f() async {} +} +''', [ + lint(19, 1), + ]); + } + + test_instanceMethod_futureVoidReturnType_arrow() async { + await assertNoDiagnostics(r''' +class Foo { + Future f() async => null; +} +'''); + } + + test_instanceMethod_futureVoidReturnType_block() async { + await assertNoDiagnostics(r''' +class Foo { + Future f() async {} +} +'''); + } + + test_instanceMethod_notAsync_arrow() async { + await assertNoDiagnostics(r''' +class Foo { + void e() => null; +} +'''); + } + + test_instanceMethod_notAsync_block() async { + await assertNoDiagnostics(r''' +class Foo { + void f() {} +} +'''); + } + + test_instanceMethod_syncStar() async { + await assertNoDiagnostics(r''' +class Foo { + Iterable f() sync* {} +} +'''); + } + + test_instanceOperator() async { + await assertDiagnostics(r''' +class Foo { + void operator |(_) async => null; +} +''', [ + lint(28, 1), + ]); + } + + test_instanceOperator_notAsync() async { + await assertNoDiagnostics(r''' +class Foo { + void operator &(_) => null; +} +'''); + } + + test_main() async { + await assertNoDiagnostics(r''' +Future f() async { } +void main() async { + await f(); +} +'''); + } + + test_notAsync_arrow() async { + await assertNoDiagnostics(r''' +void f() { + () => null; +} +'''); + } + + test_notAsync_arrow_topLevel() async { + await assertNoDiagnostics(r''' +void e() => null; +'''); + } + + test_notAsync_body() async { + await assertNoDiagnostics(r''' +void f() { + () {}; +} +'''); + } + + test_notAsync_voidReturnType() async { + await assertNoDiagnostics(r''' +void f() {} +'''); + } + + test_setter_notAsync() async { + await assertNoDiagnostics(r''' +void set f(_) => null; +'''); + } + + test_setter_notAsync_implicitReturnType() async { + await assertNoDiagnostics(r''' +set f(_) => null; +'''); + } + + test_staticMethod() async { + await assertDiagnostics(r''' +class Foo { + static void f() async {} +} +''', [ + lint(26, 1), + ]); + } + + test_staticMethod_futureVoidReturnType() async { + await assertNoDiagnostics(r''' +class Foo { + static Future f() async {} +} +'''); + } + + test_staticMethod_notAsync() async { + await assertNoDiagnostics(r''' +class Foo { + static void f() {} +} +'''); + } + + test_syncStar() async { + await assertNoDiagnostics(r''' +Iterable f() sync* {} +'''); + } + + test_typedef_futureVoidReturnType() async { + await assertNoDiagnostics(r''' +typedef Future F(int x); +'''); + } + + test_typedef_voidReturnType() async { + await assertNoDiagnostics(r''' +typedef void F(int x); +'''); + } + + test_voidReturnType_arrow() async { + await assertDiagnostics(r''' +void f() async => null; +''', [ + lint(5, 1), + ]); + } + + test_voidReturnType_block() async { + await assertDiagnostics(r''' +void f() async {} +''', [ + lint(5, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/await_only_futures_test.dart b/pkg/linter/test/rules/await_only_futures_test.dart new file mode 100644 index 00000000000..70224f7ffbd --- /dev/null +++ b/pkg/linter/test/rules/await_only_futures_test.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AwaitOnlyFuturesTest); + }); +} + +@reflectiveTest +class AwaitOnlyFuturesTest extends LintRuleTest { + @override + String get lintRule => 'await_only_futures'; + + test_undefinedClass() async { + await assertDiagnostics(r''' +Undefined f() async => await f(); +''', [ + // No lint + error(CompileTimeErrorCode.UNDEFINED_CLASS, 0, 9), + ]); + } +} diff --git a/pkg/linter/test/rules/camel_case_extensions_test.dart b/pkg/linter/test/rules/camel_case_extensions_test.dart new file mode 100644 index 00000000000..bcc91a6e111 --- /dev/null +++ b/pkg/linter/test/rules/camel_case_extensions_test.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CamelCaseExtensionsTest); + }); +} + +@reflectiveTest +class CamelCaseExtensionsTest extends LintRuleTest { + @override + String get lintRule => 'camel_case_extensions'; + + test_lowerCase() async { + await assertDiagnostics(r''' +extension fooBar on Object {} +''', [ + lint(10, 6), + ]); + } + + test_underscore() async { + await assertDiagnostics(r''' +extension Foo_Bar on Object { } +''', [ + lint(10, 7), + ]); + } + + test_unnamed() async { + await assertNoDiagnostics(r''' +extension on Object { } +'''); + } + + test_wellFormed() async { + await assertNoDiagnostics(r''' +extension FooBar on Object { } +'''); + } +} diff --git a/pkg/linter/test/rules/cancel_subscriptions_test.dart b/pkg/linter/test/rules/cancel_subscriptions_test.dart new file mode 100644 index 00000000000..1d1b8ca50ed --- /dev/null +++ b/pkg/linter/test/rules/cancel_subscriptions_test.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CancelSubscriptionsTest); + + // TODO(srawlins): Test more things: + // * null-aware cancel (`?.cancel()`) and cascade cancel (`..cancel()`) + // * StreamSubscription as a parameter + // * StreamSubscription as a top-level variable + // * public fields + // * cancel tear-off (`.cancel;`) + // * StreamSubscription subtypes + // * field initialized in a field initializer and in a field formal + // parameter + // * multiple declarations in a field declaration list and a variable + // declaration list + // * StreamSubscription being passed into a function + // * subscription canceled in a part + // * subscription declared in a part + }); +} + +@reflectiveTest +class CancelSubscriptionsTest extends LintRuleTest { + @override + String get lintRule => 'cancel_subscriptions'; + + test_localVariable_canceled() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f(Stream stream) { + StreamSubscription s = stream.listen((_) {}); + s.cancel(); +} +'''); + } + + test_localVariable_notCanceled() async { + await assertDiagnostics(r''' +import 'dart:async'; +void f(Stream stream) { + StreamSubscription s = stream.listen((_) {}); +} +''', [ + lint(66, 25), + ]); + } + + test_privateField_canceled() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +class A { + late StreamSubscription _subscription; + void f(Stream stream) { + _subscription = stream.listen((_) {}); + } + void g() { + _subscription.cancel(); + } +} +'''); + } + + test_privateField_canceled_nullable() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +class A { + StreamSubscription? _subscription; + void f(Stream stream) { + _subscription = stream.listen((_) {}); + } + void g() { + _subscription!.cancel(); + } +} +'''); + } + + @FailingTest( + reason: + 'It seems this is not implemented. In the previous incarnation of ' + 'this test, at `test_data/integration/cancel_subscriptions.dart`, an ' + 'end-of-line comment (`// OK`) indicated that the subscription ' + 'should be counted as canceled. However, that is not how an ' + 'expectation is set in an integration test, and a Lint is indeed ' + 'reported for this case. We have the test written here with what was ' + 'presumably the intention of the previous test.') + test_privateField_canceled_outsideTheClass() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +class A { + late StreamSubscription _subscription; + void f(Stream stream) { + _subscription = stream.listen((_) {}); + } +} +class B { + A a = A(); + B() { + a._subscription.cancel(); + } +} +'''); + } + + test_privateField_notCanceled() async { + await assertDiagnostics(r''' +import 'dart:async'; +class A { + late StreamSubscription _subscription; + void f(Stream stream) { + _subscription = stream.listen((_) {}); + } +} +''', [ + error(WarningCode.UNUSED_FIELD, 57, 13), + lint(57, 13), + ]); + } +} diff --git a/pkg/linter/test/rules/cast_nullable_to_non_nullable_test.dart b/pkg/linter/test/rules/cast_nullable_to_non_nullable_test.dart new file mode 100644 index 00000000000..99e1d40b6ab --- /dev/null +++ b/pkg/linter/test/rules/cast_nullable_to_non_nullable_test.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CastNullableToNonNullableTest); + }); +} + +@reflectiveTest +class CastNullableToNonNullableTest extends LintRuleTest { + @override + String get lintRule => 'cast_nullable_to_non_nullable'; + + test_castNullable() async { + await assertDiagnostics(r''' +String? s; +var a = s as String; +''', [ + lint(19, 11), + ]); + } + + test_castNullable_unresolvedType() async { + await assertDiagnostics(r''' +Undefined? s; +var a = s! as String; +''', [ + // No lint. + error(CompileTimeErrorCode.UNDEFINED_CLASS, 0, 9), + ]); + } +} diff --git a/pkg/linter/test/rules/collection_methods_unrelated_type_test.dart b/pkg/linter/test/rules/collection_methods_unrelated_type_test.dart new file mode 100644 index 00000000000..e4c64ac3872 --- /dev/null +++ b/pkg/linter/test/rules/collection_methods_unrelated_type_test.dart @@ -0,0 +1,274 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/util/dart_type_utilities.dart'; +import 'package:linter/src/util/unrelated_types_visitor.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CollectionMethodsUnrelatedTypeIterableTest); + defineReflectiveTests(CollectionMethodsUnrelatedTypeListTest); + defineReflectiveTests(CollectionMethodsUnrelatedTypeMapTest); + defineReflectiveTests(CollectionMethodsUnrelatedTypeQueueTest); + defineReflectiveTests(CollectionMethodsUnrelatedTypeSetTest); + }); +} + +/// This test has the most coverage of the various [UnrelatedTypesProcessors] +/// subclasses. 99% of the complexity in each "interface/method" case is found +/// in [UnrelatedTypesProcessors] and [typesAreUnrelated], so we do not +/// duplicate all of the test cases for [Iterable.contains] in the test cases +/// for [List.remove], etc. +@reflectiveTest +class CollectionMethodsUnrelatedTypeIterableTest extends LintRuleTest { + @override + String get lintRule => 'collection_methods_unrelated_type'; + + test_contains_related_dynamic() async { + await assertNoDiagnostics('''var x = [].contains('1' as dynamic);'''); + } + + test_contains_related_implicitTarget() async { + await assertNoDiagnostics(''' +abstract class C implements List { + void f() { + contains(1); + } +} +'''); + } + + test_contains_related_ListOfDynamic() async { + await assertNoDiagnostics('var x = [].contains(1);'); + } + + test_contains_related_mixedInto() async { + await assertNoDiagnostics(''' +mixin M {} +class C with M {} +late M m; +var x = [].contains(m); +'''); + } + + test_contains_related_mixesIn() async { + await assertNoDiagnostics(''' +mixin M {} +class C with M {} +var x = [].contains(C()); +'''); + } + + test_contains_related_null() async { + await assertNoDiagnostics('var x = [].contains(null);'); + } + + test_contains_related_Object() async { + await assertNoDiagnostics('var x = [].contains(Object());'); + } + + test_contains_related_records() async { + await assertNoDiagnostics('var x = <(num, num)>[].contains((1, 2));'); + } + + test_contains_related_subclassOfList() async { + await assertNoDiagnostics(''' +abstract class C implements List {} +void f(C c) { + c.contains(1); +} +'''); + } + + test_contains_related_subtype() async { + await assertNoDiagnostics('var x = [].contains(1);'); + } + + test_contains_related_thisTarget() async { + await assertNoDiagnostics(''' +abstract class C implements List { + void f() { + this.contains(1); + } +} +'''); + } + + test_contains_unrelated() async { + await assertDiagnostics( + '''var x = [].contains('1');''', + [lint(25, 3)], + ); + } + + test_contains_unrelated_cascade() async { + await assertDiagnostics( + '''var x = []..contains('1');''', + [lint(26, 3)], + ); + } + + test_contains_unrelated_implicitTarget() async { + await assertDiagnostics(''' +abstract class C implements List { + void f() { + contains('1'); + } +} +''', [lint(66, 3)]); + } + + test_contains_unrelated_records() async { + await assertDiagnostics("var x = <(int, int)>[].contains(('hi', 'hey'));", [ + lint(32, 13), + ]); + } + + test_contains_unrelated_recordAndNonRecord() async { + await assertDiagnostics("var x = <(int, int)>[].contains('hi');", [ + lint(32, 4), + ]); + } + + test_contains_unrelated_subclassOfList() async { + await assertDiagnostics(''' +abstract class C implements List {} +void f(C c) { + c.contains('1'); +} +''', [lint(68, 3)]); + } + + test_contains_unrelated_thisTarget() async { + await assertDiagnostics(''' +abstract class C implements List { + void f() { + this.contains('1'); + } +} +''', [lint(71, 3)]); + } +} + +@reflectiveTest +class CollectionMethodsUnrelatedTypeListTest extends LintRuleTest { + @override + String get lintRule => 'collection_methods_unrelated_type'; + + test_remove_related_subtype() async { + await assertNoDiagnostics('var x = [].remove(1);'); + } + + test_remove_unrelated() async { + await assertDiagnostics( + '''var x = [].remove('1');''', + [lint(23, 3)], + ); + } +} + +@reflectiveTest +class CollectionMethodsUnrelatedTypeMapTest extends LintRuleTest { + @override + String get lintRule => 'collection_methods_unrelated_type'; + + test_containsKey_related_subtype() async { + await assertNoDiagnostics('var x = {}.containsKey(1);'); + } + + test_containsKey_unrelated() async { + await assertDiagnostics( + '''var x = {}.containsKey('1');''', + [lint(36, 3)], + ); + } + + test_containsValue_related_subtype() async { + await assertNoDiagnostics('var x = {}.containsValue(1);'); + } + + test_containsValue_unrelated() async { + await assertDiagnostics( + '''var x = {}.containsValue('1');''', + [lint(38, 3)], + ); + } + + test_index_related_subtype() async { + await assertNoDiagnostics('var x = {}[1];'); + } + + test_index_unrelated() async { + await assertDiagnostics( + '''var x = {}['1'];''', + [lint(24, 3)], + ); + } + + test_remove_related_subtype() async { + await assertNoDiagnostics('var x = {}.remove(1);'); + } + + test_remove_unrelated() async { + await assertDiagnostics( + '''var x = {}.remove('1');''', + [lint(31, 3)], + ); + } +} + +@reflectiveTest +class CollectionMethodsUnrelatedTypeQueueTest extends LintRuleTest { + @override + String get lintRule => 'collection_methods_unrelated_type'; + + test_remove_related_subtype() async { + await assertNoDiagnostics(''' +import 'dart:collection'; +void f(Queue queue) { + queue.remove(1); +} +'''); + } + + test_remove_unrelated() async { + await assertDiagnostics(''' +import 'dart:collection'; +void f(Queue queue) { + queue.remove('1'); +} +''', [lint(68, 3)]); + } +} + +@reflectiveTest +class CollectionMethodsUnrelatedTypeSetTest extends LintRuleTest { + @override + String get lintRule => 'collection_methods_unrelated_type'; + + test_lookup_related_subtype() async { + await assertNoDiagnostics('var x = {}.lookup(1);'); + } + + test_lookup_unrelated() async { + await assertDiagnostics( + '''var x = {}.lookup('1');''', + [lint(23, 3)], + ); + } + + test_remove_related_subtype() async { + await assertNoDiagnostics('var x = {}.remove(1);'); + } + + test_remove_unrelated() async { + await assertDiagnostics( + '''var x = {}.remove('1');''', + [lint(23, 3)], + ); + } +} diff --git a/pkg/linter/test/rules/comment_references_test.dart b/pkg/linter/test/rules/comment_references_test.dart new file mode 100644 index 00000000000..6c3256a056f --- /dev/null +++ b/pkg/linter/test/rules/comment_references_test.dart @@ -0,0 +1,130 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(CommentReferencesTest); + }); +} + +@reflectiveTest +class CommentReferencesTest extends LintRuleTest { + @override + String get lintRule => 'comment_references'; + + test_false() async { + await assertDiagnostics(r''' +/// [false] +class C {} +''', [ + lint(5, 5), + ]); + } + + test_field() async { + await assertNoDiagnostics(r''' +class A { + int x = 0; + /// Assigns to [x]. + void m() {} +} +'''); + } + + test_importedElement() async { + await assertNoDiagnostics(r''' +/// [String] is OK. +class C {} +'''); + } + + test_markdown_inlineLink() async { + await assertNoDiagnostics(r''' +/// A link to [Sha256](http://tools.ietf.org/html/rfc6234) hash function. +class C {} +'''); + } + + test_markdown_inlineLink_withTitle() async { + await assertNoDiagnostics(r''' +/// A link to [Sha256](http://tools.ietf.org/html/rfc6234 "Some") hash function. +class C {} +'''); + } + + test_markdown_referenceLink() async { + await assertNoDiagnostics(r''' +/// A link to [Sha256][rfc] hash function. +/// +/// [rfc]: http://tools.ietf.org/html/rfc6234 +class C {} +'''); + } + + test_markdown_referenceLink_shortcut() async { + await assertNoDiagnostics(r''' +/// A link to [rfc][] hash function. +/// +/// [rfc]: http://tools.ietf.org/html/rfc6234 +class C {} +'''); + } + + test_null() async { + await assertDiagnostics(r''' +/// [null] +class C {} +''', [ + lint(5, 4), + ]); + } + + test_parameter() async { + await assertNoDiagnostics(r''' +class A { + /// Reads [x]. + void m(int x) {} +} +'''); + } + + test_this() async { + await assertDiagnostics(r''' +/// [this] +class C {} +''', [ + lint(5, 4), + ]); + } + + test_true() async { + await assertDiagnostics(r''' +/// [true] +class C {} +''', [ + lint(5, 4), + ]); + } + + test_unclosedSquareBracket() async { + await assertNoDiagnostics(r''' +/// [ +/// ^--- Should not crash (#819). +class C {} +'''); + } + + test_unknownElement() async { + await assertDiagnostics(r''' +/// Parameter [y]. +void f(int x) {} +''', [ + lint(15, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/conditional_uri_does_not_exist_test.dart b/pkg/linter/test/rules/conditional_uri_does_not_exist_test.dart new file mode 100644 index 00000000000..d6e9506a4a8 --- /dev/null +++ b/pkg/linter/test/rules/conditional_uri_does_not_exist_test.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ConditionalUriDoesNotExistTest); + }); +} + +@reflectiveTest +class ConditionalUriDoesNotExistTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'conditional_uri_does_not_exist'; + + test_missingDartLibraries() async { + await assertDiagnostics( + r''' +import '' + if (dart.library.io) 'dart:missing_1' + if (dart.library.html) 'dart:async' + if (dart.library.async) 'dart:missing_2'; +''', + [ + error(WarningCode.UNUSED_IMPORT, 7, 2), + lint(35, 16, messageContains: 'dart:missing_1'), + lint(120, 16, messageContains: 'dart:missing_2'), + ], + ); + } + + test_missingFiles() async { + newFile2('$testPackageRootPath/lib/exists.dart', ''); + + await assertDiagnostics( + r''' +import '' + if (dart.library.io) 'missing_1.dart' + if (dart.library.html) 'exists.dart' + if (dart.library.async) 'missing_2.dart'; +''', + [ + error(WarningCode.UNUSED_IMPORT, 7, 2), + lint(35, 16, messageContains: 'missing_1.dart'), + lint(121, 16, messageContains: 'missing_2.dart'), + ], + ); + } + + test_missingPackages() async { + await assertDiagnostics( + r''' +import '' + if (dart.library.io) 'package:meta/missing_1.dart' + if (dart.library.html) 'package:meta/meta.dart' + if (dart.library.io) 'package:foo/missing_2.dart'; +''', + [ + error(WarningCode.UNUSED_IMPORT, 7, 2), + lint(35, 29, messageContains: 'missing_1.dart'), + lint(142, 28, messageContains: 'missing_2.dart'), + ], + ); + } +} diff --git a/pkg/linter/test/rules/constant_identifier_names_test.dart b/pkg/linter/test/rules/constant_identifier_names_test.dart new file mode 100644 index 00000000000..2f9cc6e0f40 --- /dev/null +++ b/pkg/linter/test/rules/constant_identifier_names_test.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ConstantIdentifierNamesRecordsTest); + defineReflectiveTests(ConstantIdentifierNamesPatternsTest); + }); +} + +@reflectiveTest +class ConstantIdentifierNamesPatternsTest extends LintRuleTest { + @override + String get lintRule => 'constant_identifier_names'; + + test_destructuredConstField() async { + await assertDiagnostics(r''' +class A { + static const AA = (1, ); +} +''', [ + lint(25, 2), + ]); + } + + test_destructuredConstVariable() async { + await assertDiagnostics(r''' +const AA = (1, ); +''', [ + lint(6, 2), + ]); + } + + test_destructuredFinalVariable() async { + await assertDiagnostics(r''' +void f() { + final (AA, ) = (1, ); +} +''', [ + lint(20, 2), + ]); + } + + test_destructuredObjectField_switch() async { + await assertDiagnostics(r''' +class A { + var a; +} + +f(A a) { + switch (a) { + case A(a: int a_b): + } + switch (a) { + case A(a: int a_b?): + case A(a: int a_b!): + } +} +''', [ + lint(64, 3), + lint(107, 3), + lint(132, 3), + ]); + } + + test_destructuredObjectField_switch_ok() async { + await assertNoDiagnostics(r''' +class A { + var a_b; +} + +f(A a) { + switch (a) { + case A(:var a_b): + } + switch (a) { + case A(:var a_b?): + case A(:var a_b!): + } +} +'''); + } +} + +@reflectiveTest +class ConstantIdentifierNamesRecordsTest extends LintRuleTest { + @override + String get lintRule => 'constant_identifier_names'; + + test_recordFieldDestructured() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case (x: int x_x, z: int z)) { } +} +''', [ + lint(35, 3), + ]); + } + + test_recordFieldDestructured_ok() async { + await assertNoDiagnostics(r''' +f(Object o) { + if (o case (x_y: int x, z: int z)) { } +} +'''); + } + + test_recordTypeDeclarations() async { + await assertDiagnostics(r''' +const RR = (x: 1); +''', [ + lint(6, 2), + ]); + } + + test_recordTypeDeclarations_ok() async { + await assertNoDiagnostics(r''' +const r = (x: 1); +'''); + } +} diff --git a/pkg/linter/test/rules/dangling_library_doc_comments_test.dart b/pkg/linter/test/rules/dangling_library_doc_comments_test.dart new file mode 100644 index 00000000000..98fd16d7f7d --- /dev/null +++ b/pkg/linter/test/rules/dangling_library_doc_comments_test.dart @@ -0,0 +1,150 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DanglingLibraryDocCommentsTest); + }); +} + +@reflectiveTest +class DanglingLibraryDocCommentsTest extends LintRuleTest { + @override + String get lintRule => 'dangling_library_doc_comments'; + + test_docComment_aboveDeclaration() async { + await assertDiagnostics( + r''' +/// Doc comment. + +class C {} +''', + [lint(0, 16)], + ); + } + + test_docComment_aboveDeclaration_endingInReference() async { + await assertNoDiagnostics(r''' +/// Doc comment [C] +class C {} +'''); + } + + test_docComment_aboveDeclarationWithAnnotation() async { + await assertNoDiagnostics(r''' +/// Doc comment. +@deprecated +class C {} +'''); + } + + test_docComment_aboveDeclarationWithDocComment() async { + await assertDiagnostics( + r''' +/// Library comment. + +/// Class comment. +class C {} +''', + [lint(0, 20)], + ); + } + + test_docComment_aboveDeclarationWithOtherComment1() async { + await assertNoDiagnostics(r''' +/// Doc comment. +// Comment. +class C {} +'''); + } + + test_docComment_aboveDeclarationWithOtherComment2() async { + await assertDiagnostics( + r''' +/// Doc comment. + +// Comment. +class C {} +''', + [lint(0, 16)], + ); + } + + test_docComment_aboveDeclarationWithOtherComment3() async { + await assertDiagnostics( + r''' +/// Doc comment. +// Comment. + +class C {} +''', + [lint(0, 16)], + ); + } + + test_docComment_aboveDeclarationWithOtherComment4() async { + await assertNoDiagnostics(r''' +/// Doc comment. +// Comment. +/* Comment 2. */ +class C {} +'''); + } + + test_docComment_atEndOfFile() async { + await assertDiagnostics( + r''' +/// Doc comment with [int]. +''', + [lint(0, 27)], + ); + } + + test_docComment_atEndOfFile_precededByComment() async { + await assertDiagnostics( + r''' +// Copyright something. + +/// Doc comment with [int]. +''', + [lint(25, 27)], + ); + } + + test_docComment_attachedToDeclaration() async { + await assertNoDiagnostics(r''' +/// Doc comment. +class C {} +'''); + } + + test_docComment_onFirstDirective() async { + await assertDiagnostics( + r''' +/// Doc comment. +export 'dart:math'; +''', + [lint(0, 16)], + ); + } + + test_docComment_onLaterDirective() async { + await assertNoDiagnostics(r''' +export 'dart:math'; +/// Doc comment for some reason. +export 'dart:io'; +'''); + } + + test_docComment_onLibraryDirective() async { + await assertNoDiagnostics(r''' +/// Doc comment. +library l; +'''); + } +} diff --git a/pkg/linter/test/rules/depend_on_referenced_packages_test.dart b/pkg/linter/test/rules/depend_on_referenced_packages_test.dart new file mode 100644 index 00000000000..e53fcb1ddf5 --- /dev/null +++ b/pkg/linter/test/rules/depend_on_referenced_packages_test.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DependOnReferencedPackagesTest); + }); +} + +@reflectiveTest +class DependOnReferencedPackagesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'depend_on_referenced_packages'; + + String get sourceReferencingMeta => r''' +import 'package:meta/meta.dart'; + +@visibleForTesting +class C {} +'''; + + test_referencedInBin_listedInDevDeps() async { + newFile(testPackagePubspecPath, r''' +name: fancy +version: 1.1.1 + +dev_dependencies: + meta: any +'''); + var binFile = + newFile('$testPackageRootPath/bin/bin.dart', sourceReferencingMeta); + result = await resolveFile(binFile.path); + await assertDiagnosticsIn(result.errors, [ + lint(7, 24), + ]); + } + + test_referencedInLib_flutterGen() async { + var packageConfigBuilder = PackageConfigFileBuilder(); + packageConfigBuilder.add( + name: 'flutter_gen', + rootPath: '$workspaceRootPath/flutter_gen', + ); + writeTestPackageConfig(packageConfigBuilder); + newFile(testPackagePubspecPath, r''' +name: test +version: 1.1.1 +'''); + newFile('$workspaceRootPath/flutter_gen/lib/gen.dart', 'var x = 1;'); + await assertNoDiagnostics(r''' +import 'package:flutter_gen/gen.dart'; + +var y = x; +'''); + } + + test_referencedInLib_listedInDeps() async { + newFile(testPackagePubspecPath, r''' +name: fancy +version: 1.1.1 + +dependencies: + meta: any +'''); + await assertNoDiagnostics(sourceReferencingMeta); + } + + test_referencedInLib_listedInDevDeps() async { + newFile(testPackagePubspecPath, r''' +name: fancy +version: 1.1.1 + +dev_dependencies: + meta: any +'''); + await assertDiagnostics(sourceReferencingMeta, [ + lint(7, 24), + ]); + } + + test_referencedInLib_missingFromPubspec() async { + newFile(testPackagePubspecPath, r''' +name: fancy +version: 1.1.1 +'''); + await assertDiagnostics(sourceReferencingMeta, [ + lint(7, 24), + ]); + } + + test_referencedInLib_selfPackage() async { + newFile(testPackagePubspecPath, r''' +name: test +version: 1.1.1 +'''); + newFile('$testPackageRootPath/lib/lib.dart', 'var x = 1;'); + await assertNoDiagnostics(r''' +import 'package:test/lib.dart'; + +var y = x; +'''); + } + + test_referencedInTest_listedInDevDeps() async { + newFile(testPackagePubspecPath, r''' +name: fancy +version: 1.1.1 + +dev_dependencies: + meta: any +'''); + var testFile = + newFile('$testPackageRootPath/test/test.dart', sourceReferencingMeta); + result = await resolveFile(testFile.path); + await assertNoDiagnosticsIn(result.errors); + } +} diff --git a/pkg/linter/test/rules/deprecated_consistency_test.dart b/pkg/linter/test/rules/deprecated_consistency_test.dart new file mode 100644 index 00000000000..b2de35e69ed --- /dev/null +++ b/pkg/linter/test/rules/deprecated_consistency_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DeprecatedConsistencyTest); + }); +} + +@reflectiveTest +class DeprecatedConsistencyTest extends LintRuleTest { + @override + String get lintRule => 'deprecated_consistency'; + + test_superInit() async { + await assertDiagnostics(r''' +class A { + String? a; + A({@deprecated this.a}); +} + +class B extends A { + B({super.a}); +} +''', [ + lint(20, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/deprecated_member_use_from_same_package_test.dart b/pkg/linter/test/rules/deprecated_member_use_from_same_package_test.dart new file mode 100644 index 00000000000..0e49a330f3a --- /dev/null +++ b/pkg/linter/test/rules/deprecated_member_use_from_same_package_test.dart @@ -0,0 +1,786 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DeprecatedMemberUseFromSamePackageTest); + }); +} + +@reflectiveTest +class DeprecatedMemberUseFromSamePackageTest extends LintRuleTest { + @override + String get lintRule => 'deprecated_member_use_from_same_package'; + + @override + Future assertDiagnostics( + String code, List expectedDiagnostics) async { + addTestFile(code); + await resolveTestFile(); + var filteredErrors = errors + .whereNot((e) => + e.errorCode == HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE || + e.errorCode == + HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE_WITH_MESSAGE) + .toList(); + await assertDiagnosticsIn(filteredErrors, expectedDiagnostics); + } + + test_deprecatedCallMethod() async { + await assertDiagnostics(r''' +class C { + @deprecated + void call() {} +} + +void f(C c) => c(); +''', [ + lint(59, 3), + ]); + } + + test_deprecatedClass() async { + await assertDiagnostics(r''' +@deprecated +class C {} + +void f(C c) {} +''', [ + lint(31, 1), + ]); + } + + test_deprecatedClass_usedInClassTypeAlias() async { + await assertDiagnostics(r''' +@deprecated +mixin class C {} + +class D = Object with C; +''', [ + lint(52, 1), + ]); + } + + test_deprecatedClass_usedInDeprecatedClassTypeAlias() async { + await assertNoDiagnostics(r''' +@deprecated +mixin class C {} + +@deprecated +class D = Object with C; +'''); + } + + test_deprecatedClass_usedInDeprecatedDefaultParameter() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +void f({@deprecated C? c = null}) {} +'''); + } + + test_deprecatedClass_usedInDeprecatedEnum() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +enum E { + one, two; + + void f(C c) {} +} +'''); + } + + test_deprecatedClass_usedInDeprecatedExtension() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +extension E on C {} +'''); + } + + test_deprecatedClass_usedInDeprecatedField_initializer() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +class D { + @deprecated + Object f = C; +} +'''); + } + + test_deprecatedClass_usedInDeprecatedField_typeAnnotation() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +class D { + @deprecated + C? f; +} +'''); + } + + test_deprecatedClass_usedInDeprecatedFieldFormalParameter() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +class D { + Object c; + D({@deprecated required C this.c}); +} +'''); + } + + test_deprecatedClass_usedInDeprecatedFunctionTypeAlias() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +typedef void Callback(C c); +'''); + } + + test_deprecatedClass_usedInDeprecatedFunctionTypedParameter() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +void f({@deprecated required void p(C c)}) {} +'''); + } + + test_deprecatedClass_usedInDeprecatedLibrary() async { + await assertNoDiagnostics(r''' +@deprecated +library a; + +@deprecated +class C {} + +C? x; +'''); + } + + test_deprecatedClass_usedInDeprecatedMixin() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +mixin M { + C? x; +} +'''); + } + + test_deprecatedClass_usedInDeprecatedSimpleParameter() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +void f({@deprecated C? c}) {} +'''); + } + + test_deprecatedClass_usedInDeprecatedTopLevelVariable() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +C? x; +'''); + } + + test_deprecatedClass_usedInFieldFormalParameter() async { + await assertDiagnostics(r''' +@deprecated +class C {} + +class D { + Object c; + D({required C this.c}); +} +''', [ + lint(60, 1), + ]); + } + + test_deprecatedClass_usedInFunctionTypeAlias() async { + await assertDiagnostics(r''' +@deprecated +class C {} + +typedef void Callback(C c); +''', [ + lint(46, 1), + ]); + } + + test_deprecatedClass_usedInFunctionTypedParameter() async { + await assertDiagnostics(r''' +@deprecated +class C {} + +void f({void p(C c)?}) {} +''', [ + lint(39, 1), + ]); + } + + test_deprecatedClass_usedInGenericFunctionTypeAlias() async { + await assertNoDiagnostics(r''' +@deprecated +class C {} + +@deprecated +typedef Callback = void Function(C); +'''); + } + + test_deprecatedClass_usedInGenericTypeAlias() async { + await assertDiagnostics(r''' +@deprecated +class C {} + +typedef Callback = void Function(C); +''', [ + lint(57, 1), + ]); + } + + test_deprecatedClass_usedInHideCombinator() async { + newFile('$testPackageLibPath/lib.dart', r''' +@deprecated +class C {} +'''); + await assertDiagnostics(r''' +import 'lib.dart' hide C; +''', [ + // No lint. + error(WarningCode.UNUSED_IMPORT, 7, 10), + ]); + } + + test_deprecatedClass_usedInShowCombinator() async { + newFile('$testPackageLibPath/lib.dart', r''' +@deprecated +class C {} +'''); + await assertDiagnostics(r''' +import 'lib.dart' show C; +''', [ + error(WarningCode.UNUSED_IMPORT, 7, 10), + lint(23, 1), + ]); + } + + test_deprecatedConstructor_usedInSuperConstructorCall() async { + await assertDiagnostics(r''' +class A { + @deprecated + A(); +} +class B extends A { + B() : super(); +} +''', [ + lint(61, 7), + ]); + } + + test_deprecatedDefaultParameterOfFunction() async { + await assertDiagnostics(r''' +void f({@deprecated int p = 1}) {} + +void g() => f(p: 1); +''', [ + lint(50, 1), + ]); + } + + test_deprecatedEnum() async { + await assertDiagnostics(r''' +@deprecated +enum E { + one, two; +} +late E e; +''', [ + lint(40, 1), + ]); + } + + test_deprecatedEnumValue() async { + await assertDiagnostics(r''' +enum E { + one, @deprecated two; +} +late E e = E.two; +''', [ + lint(48, 3), + ]); + } + + test_deprecatedExtension_usedInExtensionOverride_getter() async { + await assertDiagnostics(r''' +@deprecated +extension E on int { + int get foo => 1; +} + +var x = E(0).foo; +''', [ + lint(64, 1), + ]); + } + + test_deprecatedExtension_usedInExtensionOverride_methodInvocation() async { + await assertDiagnostics(r''' +@deprecated +extension E on int { + void f() {} +} + +var x = E(0).f(); +''', [ + lint(58, 1), + ]); + } + + test_deprecatedField_usedAsGetter() async { + await assertDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +void f(A a) { + a.f; +} +''', [ + lint(58, 1), + ]); + } + + test_deprecatedField_usedAsSetter() async { + await assertDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +void f(A a) { + a.f = 1; +} +''', [ + lint(58, 1), + ]); + } + + test_deprecatedField_usedInDeprecatedClass() async { + await assertNoDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +@deprecated +class B { + void f(A a) { + a.f; + a.f = 1; + } +} +'''); + } + + test_deprecatedField_usedInDeprecatedFunction() async { + await assertNoDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +@deprecated +void f(A a) { + a.f; + a.f = 1; +} +'''); + } + + test_deprecatedField_usedInPostfix() async { + await assertDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +void f(A a) { + a.f++; +} +''', [ + lint(56, 3), + ]); + } + + test_deprecatedField_usedInPrefix() async { + await assertDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +void f(A a) { + ++a.f; +} +''', [ + lint(58, 3), + ]); + } + + test_deprecatedField_usedInSuper() async { + await assertDiagnostics(r''' +class A { + @deprecated + int f = 0; +} + +class B extends A { + int get g => super.f; +} +''', [ + lint(81, 1), + ]); + } + + test_deprecatedGetter_usedInAssignment() async { + await assertNoDiagnostics(r''' +@deprecated +int get x => 1; + +set x(int value) {} + +void f() { + x = 1; +} +'''); + } + + test_deprecatedGetter_usedInCompoundAssignment() async { + await assertDiagnostics(r''' +@deprecated +int get x => 1; + +set x(int value) {} + +void f() { + x += 1; +} +''', [ + lint(63, 1), + ]); + } + + test_deprecatedIndexOperator() async { + await assertDiagnostics(r''' +class C { + @deprecated + int operator[](int p) => 1; +} + +void f(C c) { + c[1]; +} +''', [ + lint(73, 4), + ]); + } + + test_deprecatedLibrary_export() async { + newFile('$testPackageLibPath/lib.dart', r''' +@deprecated +library a; +'''); + await assertDiagnostics(r''' +export 'lib.dart'; +''', [ + lint(0, 18), + ]); + } + + test_deprecatedLibrary_import() async { + newFile('$testPackageLibPath/lib.dart', r''' +@deprecated +library a; +'''); + await assertDiagnostics(r''' +import 'lib.dart'; +''', [ + lint(0, 18), + error(WarningCode.UNUSED_IMPORT, 7, 10), + ]); + } + + test_deprecatedMethod() async { + await assertDiagnostics(r''' +class A { + @deprecated + void m() {} + + void m2() { + m(); + } +} +''', [ + lint(57, 1), + ]); + } + + test_deprecatedMethod_usedInDeprecatedConstructor() async { + await assertNoDiagnostics(r''' +class A { + @deprecated + void m() {} + + @deprecated + A() { + m(); + } +} +'''); + } + + test_deprecatedMethod_usedInDeprecatedSubclassConstructor() async { + await assertNoDiagnostics(r''' +class A { + @deprecated + void m() {} +} +class B extends A { + @deprecated + B() { + m(); + } +} +'''); + } + + test_deprecatedMethod_withMessage() async { + await assertDiagnostics(r''' +class A { + @Deprecated('Message') + void m() {} + + void m2() { + m(); + } +} +''', [ + lint(68, 1), + ]); + } + + test_deprecatedNamedConstructor() async { + await assertDiagnostics(r''' +class C { + @deprecated + C.named(); +} + +var x = C.named(); +''', [ + lint(48, 7), + ]); + } + + test_deprecatedNamedConstructor_usedInSuperConstructorCall() async { + await assertDiagnostics(r''' +class A { + @deprecated + A.named(); +} +class B extends A { + B() : super.named(); +} +''', [ + lint(67, 13), + ]); + } + + test_deprecatedNamedParameterOfFunction() async { + await assertDiagnostics(r''' +void f({@deprecated int? p}) {} + +void g() => f(p: 1); +''', [ + lint(47, 1), + ]); + } + + test_deprecatedNamedParameterOfFunction_usedInDeclaringFunction() async { + await assertNoDiagnostics(r''' +int? f({@deprecated int? p}) => p; +'''); + } + + test_deprecatedNamedParameterOfMethod() async { + await assertDiagnostics(r''' +class C { + void f({@deprecated int? p}) {} + + void g() => f(p: 1); +} +''', [ + lint(61, 1), + ]); + } + + test_deprecatedOperator() async { + await assertDiagnostics(r''' +class C { + @deprecated + C operator+(int other) => C(); +} +void f(C c) { + c + 1; +} +''', [ + lint(75, 5), + ]); + } + + test_deprecatedOperator_usedInCompoundAssignment() async { + await assertDiagnostics(r''' +class C { + @deprecated + C operator+(int other) => C(); +} +void f(C c) { + c += 1; +} +''', [ + lint(75, 6), + ]); + } + + test_deprecatedParameterOfConstructor_usedInDeclaringConstructorBody() async { + await assertNoDiagnostics(r''' +class C { + C({@deprecated int? p}) { + p; + } +} +'''); + } + + test_deprecatedParameterOfConstructor_usedInDeclaringConstructorInitializer() async { + await assertNoDiagnostics(r''' +class C { + C({@deprecated int? p}) : assert(p == null || p > 0); +} +'''); + } + + test_deprecatedParameterOfConstructor_usedInRedirectingConstructor() async { + await assertDiagnostics(r''' +class C { + C({@deprecated int? p}); + C.two() : this(p: 0); +} +''', [ + lint(54, 1), + ]); + } + + test_deprecatedPositionalParameterOfFunction() async { + await assertDiagnostics(r''' +void f([@deprecated int? p]) {} + +void g() => f(1); +''', [ + lint(47, 1), + ]); + } + + test_deprecatedSetter() async { + await assertDiagnostics(r''' +@deprecated set f(int value) {} + +void g() => f = 1; +''', [ + lint(45, 1), + ]); + } + + test_deprecatedSetter_usedInCompoundAssignment() async { + await assertDiagnostics(r''' +int get x => 1; + +@deprecated +set x(int value) {} + +void f() { + x += 1; +} +''', [ + lint(63, 1), + ]); + } + + test_deprecatedStaticField() async { + await assertDiagnostics(r''' +class A { + @deprecated + static int f = 0; +} + +var a = A.f; +''', [ + lint(57, 1), + ]); + } + + test_deprecatedTopLevelVariable_usedInAssignment() async { + await assertDiagnostics(r''' +@deprecated +int x = 1; + +void f() { + x = 1; +} +''', [ + lint(37, 1), + ]); + } + + test_deprecatedUnnamedConstructor() async { + await assertDiagnostics(r''' +class C { + @deprecated + C(); +} + +var x = C(); +''', [ + lint(42, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/directives_ordering_test.dart b/pkg/linter/test/rules/directives_ordering_test.dart new file mode 100644 index 00000000000..f15f2c5d06e --- /dev/null +++ b/pkg/linter/test/rules/directives_ordering_test.dart @@ -0,0 +1,297 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DirectivesOrderingTest); + }); +} + +@reflectiveTest +class DirectivesOrderingTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + bool get addJsPackageDep => true; + + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'directives_ordering'; + + test_dartDirectivesGoFirst_exports() async { + newFile2('$testPackageLibPath/a.dart', ''); + await assertDiagnostics(r''' +export 'dart:math'; +export 'a.dart'; +export 'dart:html'; +export 'dart:isolate'; +// ignore_for_file: unused_import +''', [ + lint(37, 19), + lint(57, 22), + ]); + } + + test_dartDirectivesGoFirst_imports() async { + newFile2('$testPackageLibPath/a.dart', ''); + await assertDiagnostics(r''' +import 'dart:math'; +import 'a.dart'; +import 'dart:html'; +import 'dart:isolate'; +// ignore_for_file: unused_import +''', [ + lint(37, 19), + lint(57, 22), + ]); + } + + test_importsGoBeforeExports() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + newFile2('$testPackageLibPath/c.dart', ''); + await assertDiagnostics(r''' +import 'a.dart'; + +export 'a.dart'; + +import 'b.dart'; + +export 'b.dart'; + +import 'c.dart'; + +export 'c.dart'; +// ignore_for_file: unused_import +''', [ + lint(18, 16), + lint(54, 16), + ]); + } + + test_multipleSchemaImportsSortedByPath() async { + newFile2('$testPackageLibPath/a.dart', ''); + await assertNoDiagnostics(r''' +// From analysis_server/test/services/correction/organize_directives_test.dart +// test named `sort`. +library lib; + +import 'dart:aaa'; +import 'dart:bbb'; + +import 'package:aaa/aaa.dart'; +import 'package:bbb/bbb.dart'; + +import 'http://aaa.com'; +import 'http://bbb.com'; + +import 'aaa/aaa.dart'; +import 'bbb/bbb.dart'; + +export 'dart:aaa'; +export 'dart:bbb'; + +export 'package:aaa/aaa.dart'; +export 'package:bbb/bbb.dart'; + +export 'http://aaa.com'; +export 'http://bbb.com'; + +export 'aaa/aaa.dart'; +export 'bbb/bbb.dart'; + +part 'aaa/aaa.dart'; +part 'bbb/bbb.dart'; + +main() {} + +// ignore_for_file: unused_import, uri_does_not_exist +'''); + } + + test_packageDirectivesGoBeforeRelative_exports() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + await assertDiagnostics(r''' +export 'dart:math'; +export 'a.dart'; +export 'package:js/js.dart'; +export 'package:meta/meta.dart'; +export 'b.dart'; +// ignore_for_file: unused_import +''', [ + lint(37, 28), + lint(66, 32), + ]); + } + + test_packageDirectivesGoBeforeRelative_imports() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + await assertDiagnostics(r''' +import 'dart:math'; +import 'package:js/js.dart'; +import 'a.dart'; +import 'package:meta/meta.dart'; +import 'b.dart'; +// ignore_for_file: unused_import +''', [ + lint(66, 32), + ]); + } + + test_packageImportsSortedByPath() async { + newFile2('$testPackageLibPath/a.dart', ''); + await assertNoDiagnostics(r''' +// From analysis_server/test/services/correction/organize_directives_test.dart +// test named `sort_imports_packageAndPath`. + +library lib; + +import 'package:product.ui/entity.dart'; +import 'package:product.ui.api/entity1.dart'; +import 'package:product.ui.api/entity2.dart'; +import 'package:product.ui.api.aaa/manager2.dart'; +import 'package:product.ui.api.bbb/manager1.dart'; +import 'package:product2.client/entity.dart'; + +// ignore_for_file: unused_import, uri_does_not_exist +'''); + } + + test_reportOneNodeOnlyOnce() async { + newFile2('$testPackageLibPath/a.dart', ''); + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; +import 'a.dart'; +import 'package:js/js.dart'; +// ignore_for_file: unused_import +''', [ + lint(50, 28), + ]); + } + + test_sortDirectiveSectionsAlphabetically_dartSchema_export() async { + await assertDiagnostics(r''' +export 'dart:isolate'; +export 'dart:convert'; +export 'dart:math'; +''', [ + lint(23, 22), + ]); + } + + test_sortDirectiveSectionsAlphabetically_dartSchema_import() async { + await assertDiagnostics(r''' +import 'dart:html'; +import 'dart:isolate'; +import 'dart:convert'; +import 'dart:math'; +// ignore_for_file: unused_import +''', [ + lint(43, 22), + ]); + } + + test_sortDirectiveSectionsAlphabetically_packageSchema_export() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + newFile2('$testPackageLibPath/c.dart', ''); + await assertDiagnostics(r''' +export 'package:js/js.dart'; +export 'package:meta/meta.dart'; +export 'package:flutter/widgets.dart'; + +export 'package:test/a.dart'; +export 'package:test/c.dart'; +export 'package:test/b.dart'; +''', [ + lint(62, 38), + lint(162, 29), + ]); + } + + test_sortDirectiveSectionsAlphabetically_packageSchema_import() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + newFile2('$testPackageLibPath/c.dart', ''); + await assertDiagnostics(r''' +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:test/a.dart'; +import 'package:test/c.dart'; +import 'package:test/b.dart'; +// ignore_for_file: unused_import +''', [ + lint(62, 38), + lint(162, 29), + ]); + } + + test_sortDirectiveSectionsAlphabetically_relativePath_export() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + newFile2('$testPackageLibPath/c.dart', ''); + newFile2('$testPackageLibPath/d.dart', ''); + await assertDiagnostics(r''' +export 'd.dart'; +export 'a.dart'; +export 'b.dart'; +export 'c.dart'; +''', [ + lint(17, 16), + ]); + } + + test_sortDirectiveSectionsAlphabetically_relativePath_import() async { + newFile2('$testPackageLibPath/a.dart', ''); + newFile2('$testPackageLibPath/b.dart', ''); + newFile2('$testPackageLibPath/c.dart', ''); + newFile2('$testPackageLibPath/d.dart', ''); + await assertDiagnostics(r''' +import 'd.dart'; +import 'd.dart'; +import 'c.dart'; +import 'c.dart'; +import 'b.dart'; +import 'b.dart'; +import 'a.dart'; +import 'a.dart'; +// ignore_for_file: duplicate_import, unused_import +''', [ + lint(34, 16), + lint(68, 16), + lint(102, 16), + ]); + } + + test_sortDirectiveSectionsAlphabetically_dotInRelativePath_import() async { + await assertDiagnostics(r''' +import './foo1.dart'; +import '../../foo2.dart'; +import '../foo3.dart'; +import 'foo4.dart'; +// ignore_for_file: unused_import, uri_does_not_exist +''', [lint(22, 25)]); + } + + test_sortDirectiveSectionsAlphabetically_dotInRelativePath_import_ok() async { + await assertNoDiagnostics(r''' +import '../../foo4.dart'; +import '../foo3.dart'; +import './foo2.dart'; +import 'foo1.dart'; +// ignore_for_file: unused_import, uri_does_not_exist +'''); + } +} diff --git a/pkg/linter/test/rules/discarded_futures_test.dart b/pkg/linter/test/rules/discarded_futures_test.dart new file mode 100644 index 00000000000..93d0591c6af --- /dev/null +++ b/pkg/linter/test/rules/discarded_futures_test.dart @@ -0,0 +1,225 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DiscardedFuturesTest); + }); +} + +@reflectiveTest +class DiscardedFuturesTest extends LintRuleTest { + @override + String get lintRule => 'discarded_futures'; + + test_assignment_ok() async { + await assertNoDiagnostics(r''' +var handler = {}; + +void ff(String command) { + handler[command] = () async { + await g(); + g(); + }; +} +Future g() async => 0; +'''); + } + + test_constructor() async { + await assertDiagnostics(r''' +class A { + A() { + g(); + } +} + +Future g() async => 0; +''', [ + lint(22, 1), + ]); + } + + test_field_assignment() async { + await assertDiagnostics(r''' +class A { + var a = () { + g(); + }; +} + +Future g() async => 0; +''', [ + lint(29, 1), + ]); + } + + test_function() async { + await assertDiagnostics(r''' +void recreateDir(String path) { + deleteDir(path); + createDir(path); +} + +Future deleteDir(String path) async {} +Future createDir(String path) async {} +''', [ + lint(34, 9), + lint(53, 9), + ]); + } + + test_function_closure() async { + await assertDiagnostics(r''' +void f() { + () { + createDir('.'); + }(); +} + +Future createDir(String path) async {} +''', [ + lint(22, 9), + ]); + } + + test_function_closure_ok() async { + await assertNoDiagnostics(r''' +Future f() async { + () { + createDir('.'); + }(); +} + +Future createDir(String path) async {} +'''); + } + + test_function_expression() async { + await assertDiagnostics(r''' +void f() { + var x = h(() => g()); + print(x); +} + +int h(Function f) => 0; + +Future g() async => 0; +''', [ + lint(29, 1), + ]); + } + + test_function_ok_async() async { + await assertNoDiagnostics(r''' +Future recreateDir(String path) async { + await deleteDir(path); + await createDir(path); +} + +Future deleteDir(String path) async {} +Future createDir(String path) async {} +'''); + } + + test_function_ok_return_invocation() async { + await assertNoDiagnostics(r''' +Future f() { + return g(); +} +Future g() async => 0; +'''); + } + + test_function_ok_unawaited() async { + await assertNoDiagnostics(r''' +import 'dart:async'; + +void recreateDir(String path) { + unawaited(deleteDir(path)); + unawaited(createDir(path)); +} + +Future deleteDir(String path) async {} +Future createDir(String path) async {} +'''); + } + + test_method() async { + await assertDiagnostics(r''' +class Dir{ + void recreateDir(String path) { + deleteDir(path); + createDir(path); + } + + Future deleteDir(String path) async {} + Future createDir(String path) async {} +} +''', [ + lint(49, 9), + lint(70, 9), + ]); + } + + test_topLevel_assignment() async { + await assertDiagnostics(r''' +var a = () { + g(); +}; + +Future g() async => 0; +''', [ + lint(15, 1), + ]); + } + + test_topLevel_assignment_expression_body() async { + await assertDiagnostics(r''' +var a = () => g(); + +Future g() async => 0; +''', [ + lint(14, 1), + ]); + } + + test_topLevel_assignment_ok_async() async { + await assertNoDiagnostics(r''' +var a = () async { + g(); +}; + +Future g() async => 0; +'''); + } + + test_topLevel_assignment_ok_future() async { + await assertNoDiagnostics(r''' +Future a = g(); + +Future g() async => 0; +'''); + } + + test_variable_assignment() async { + await assertDiagnostics(r''' +var handler = {}; + +void ff(String command) { + handler[command] = () { + g(); + }; +} + +Future g() async => 0; +''', [ + lint(93, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/do_not_use_environment_test.dart b/pkg/linter/test/rules/do_not_use_environment_test.dart new file mode 100644 index 00000000000..8457acb9c6c --- /dev/null +++ b/pkg/linter/test/rules/do_not_use_environment_test.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DoNotUseEnvironmentTest); + }); +} + +@reflectiveTest +class DoNotUseEnvironmentTest extends LintRuleTest { + @override + String get lintRule => 'do_not_use_environment'; + test_bool() async { + await assertDiagnostics(r''' +void f() { + bool.fromEnvironment('key'); +} +''', [ + lint(13, 20), + ]); + } + + test_hasEnvironment() async { + await assertDiagnostics(r''' +void f() { + bool.hasEnvironment('key'); +} +''', [ + lint(13, 19), + ]); + } + + test_int() async { + await assertDiagnostics(r''' +void f() { + int.fromEnvironment('key'); +} +''', [ + lint(13, 19), + ]); + } + + test_string() async { + await assertDiagnostics(r''' +void f() { + String.fromEnvironment('key'); +} +''', [ + lint(13, 22), + ]); + } +} diff --git a/pkg/linter/test/rules/empty_catches_test.dart b/pkg/linter/test/rules/empty_catches_test.dart new file mode 100644 index 00000000000..07c8e313099 --- /dev/null +++ b/pkg/linter/test/rules/empty_catches_test.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(EmptyCatchesTest); + }); +} + +@reflectiveTest +class EmptyCatchesTest extends LintRuleTest { + @override + String get lintRule => 'empty_catches'; + test_comment() async { + await assertNoDiagnostics(r''' +void foo() { + try { + throw new Exception(); + } catch (e) { + // Nothing. + } +} +'''); + } + + test_emptyCatch() async { + await assertDiagnostics(r''' +void foo() { + try { + throw Exception(); + } catch (e) {} +} +''', [ + lint(58, 2), + ]); + } + + test_statement() async { + await assertNoDiagnostics(r''' +void foo() { + try { + throw new Exception(); + } catch (e) { + print(e); + } +} +'''); + } + + test_underscore() async { + await assertNoDiagnostics(r''' +void foo() { + try { + throw Exception(); + } catch (_) {} +} +'''); + } +} diff --git a/pkg/linter/test/rules/empty_constructor_bodies_test.dart b/pkg/linter/test/rules/empty_constructor_bodies_test.dart new file mode 100644 index 00000000000..a1aa012aa28 --- /dev/null +++ b/pkg/linter/test/rules/empty_constructor_bodies_test.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(EmptyConstructorBodiesTest); + }); +} + +@reflectiveTest +class EmptyConstructorBodiesTest extends LintRuleTest { + @override + String get lintRule => 'empty_constructor_bodies'; + + test_empty() async { + await assertDiagnostics(r''' +class A { + A() {} +} +''', [ + lint(16, 2), + ]); + } + + test_empty_withComment() async { + await assertNoDiagnostics(r''' +class A { + A() { + // Comments make this OK! + } +} +'''); + } + + test_noBody() async { + await assertNoDiagnostics(r''' +class A { + A(); +} +'''); + } + + test_noBody_parameters() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} +'''); + } + + test_notEmpty() async { + await assertNoDiagnostics(r''' +class A { + A() { + print('hi'); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/empty_statements_test.dart b/pkg/linter/test/rules/empty_statements_test.dart new file mode 100644 index 00000000000..d690e8e9b22 --- /dev/null +++ b/pkg/linter/test/rules/empty_statements_test.dart @@ -0,0 +1,163 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(EmptyStatementsTest); + }); +} + +@reflectiveTest +class EmptyStatementsTest extends LintRuleTest { + @override + String get lintRule => 'empty_statements'; + + test_emptyFor() async { + await assertDiagnostics(r''' +void f(bool b) { + for ( ; b; ); +} +''', [ + lint(31, 1), + ]); + } + + test_emptyIf_followedByBlock() async { + await assertDiagnostics(r''' +void f(bool b) { + if (b); + { + print(b); + } +} +''', [ + lint(25, 1), + ]); + } + + test_emptyIf_followedByStatement() async { + await assertDiagnostics(r''' +void f(bool b) { + if (b); + print(b); +} +''', [ + lint(25, 1), + ]); + } + + test_emptyWhile() async { + await assertDiagnostics(r''' +void f(bool b) { + while (b); +} +''', [ + lint(28, 1), + ]); + } + + test_nonEmptyIf_emptyBlock() async { + await assertNoDiagnostics(r''' +void f(bool b) { + if (b) {} +} +'''); + } + + test_nonEmptyWhile_emptyBlock() async { + await assertNoDiagnostics(r''' +void f(bool b) { + while (b) {} +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4410 + test_switchPatternCase_justEmpties() async { + await assertDiagnostics(r''' +f() { + switch(true) { + case true : + ; + ; + ; + case false : + print(''); + } +} +''', [ + lint(49, 1), + lint(57, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4410 + test_switchPatternCase_leading() async { + await assertDiagnostics(r''' +f() { + switch(true) { + case true : + ; + print(''); + case false : + print(''); + } +} +''', [ + lint(49, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4410 + test_switchPatternCase_leading_trailing() async { + await assertDiagnostics(r''' +f() { + switch(true) { + case true : + ; + ; + case false : + print(''); + } +} +''', [ + lint(49, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4410 + test_switchPatternCase_only() async { + await assertNoDiagnostics(r''' +f() { + switch(true) { + case true : + ; + case false : + print(''); + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4410 + test_switchPatternCase_trailing() async { + await assertDiagnostics(r''' +f() { + switch(true) { + case true : + print(''); + ; + case false : + print(''); + } +} +''', [ + lint(66, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/eol_at_end_of_file_test.dart b/pkg/linter/test/rules/eol_at_end_of_file_test.dart new file mode 100644 index 00000000000..83ff4737d06 --- /dev/null +++ b/pkg/linter/test/rules/eol_at_end_of_file_test.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(EolAtEndOfFileTest); + }); +} + +@reflectiveTest +class EolAtEndOfFileTest extends LintRuleTest { + @override + String get lintRule => 'eol_at_end_of_file'; + + test_hasEol() async { + await assertDiagnostics(r''' +class A {}''', [ + lint(10, 1), + ]); + } + + test_hasMultipleNewlines() async { + await assertDiagnostics(r''' +class A {} + +''', [ + lint(10, 1), + ]); + } + + test_hasNoEol() async { + await assertNoDiagnostics(r''' +class A {} +'''); + } +} diff --git a/pkg/linter/test/rules/exhaustive_cases_test.dart b/pkg/linter/test/rules/exhaustive_cases_test.dart new file mode 100644 index 00000000000..a0f9e5a7156 --- /dev/null +++ b/pkg/linter/test/rules/exhaustive_cases_test.dart @@ -0,0 +1,265 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ExhaustiveCasesTestLanguage219); + defineReflectiveTests(ExhaustiveCasesTest); + }); +} + +abstract class BaseExhaustiveCasesTest extends LintRuleTest { + final actualEnumSource = r''' +enum ActualEnum { e, f } + +void ae(ActualEnum e) { + switch (e) { + case ActualEnum.e: + } +} +'''; + + @override + String get lintRule => 'exhaustive_cases'; + + test_enumLike() async { + await assertDiagnostics(r''' +class E { + final int i; + const E._(this.i); + + static const e = E._(1); + static const f = E._(2); + static const g = E._(3); +} + +void e(E e) { + switch (e) { + case E.e: + break; + case E.f: + } +} +''', [ + lint(147, 10), + ]); + } + + test_enumLike_default_ok() async { + await assertNoDiagnostics(r''' +class E { + final int i; + const E._(this.i); + + static const e = E._(1); + static const f = E._(2); + static const g = E._(3); +} + +void okDefault(E e) { + switch (e) { + case E.e: + break; + default: + break; + } +} +'''); + } + + test_enumLike_deprecatedFields() async { + await assertDiagnostics(r''' +class DeprecatedFields { + final int i; + const DeprecatedFields._(this.i); + + @deprecated + static const oldFoo = newFoo; + static const newFoo = DeprecatedFields._(1); + static const bar = DeprecatedFields._(2); + static const baz = DeprecatedFields._(3); +} + +void dep(DeprecatedFields e) { + switch (e) { + case DeprecatedFields.newFoo: + break; + case DeprecatedFields.bar: + break; + case DeprecatedFields.baz: + break; + } + + switch (e) { + case DeprecatedFields.newFoo: + break; + case DeprecatedFields.baz: + break; + } + + switch (e) { + case DeprecatedFields.oldFoo: + break; + case DeprecatedFields.bar: + break; + case DeprecatedFields.baz: + break; + } +} +''', [ + lint(450, 10), + error(HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE, 602, 6), + ]); + } + + test_enumLike_ok() async { + await assertNoDiagnostics(r''' +class E { + final int i; + const E._(this.i); + + static const e = E._(1); + static const f = E._(2); + static const g = E._(3); +} + +void ok(E e) { + switch (e) { + case E.e: + break; + case E.f: + break; + case E.g: + break; + } +} +'''); + } + + test_enumLike_parenthesized_ok() async { + await assertNoDiagnostics(r''' +class E { + final int i; + const E._(this.i); + + static const e = E._(1); + static const f = E._(2); + static const g = E._(3); +} + +void okParenthesized(E e) { + switch (e) { + case (E.e): + break; + case ((E.f)): + break; + case (E.g): + break; + } +} +'''); + } + + test_enumLike_prefixed() async { + newFile('$testPackageLibPath/e.dart', ''' +class E { + final int i; + const E._(this.i); + + static const e = E._(1); + static const f = E._(2); + static const g = E._(3); +} +'''); + + await assertDiagnostics(r''' +import 'e.dart' as prefixed; + +void e(prefixed.E e) { + switch(e) { + case prefixed.E.e : + print('e'); + break; + case prefixed.E.f : + print('e'); + } +} +''', [ + lint(55, 9), + ]); + } + + test_notEnumLike_ok() async { + await assertNoDiagnostics(r''' +class TooFew { + const TooFew._(); + + static const e = TooFew._(); +} + +void t(TooFew e) { + switch (e) { + case TooFew.e: + } +} + +class PublicCons { + const PublicCons(); + static const e = PublicCons(); + static const f = PublicCons(); +} + +void p(PublicCons e) { + switch (e) { + case PublicCons.e: + } +} +'''); + } + + test_notEnumLike_subclassed_ok() async { + await assertNoDiagnostics(r''' +class Subclassed { + const Subclassed._(); + + static const e = Subclassed._(); + static const f = Subclassed._(); + static const g = Subclassed._(); +} + +class Subclass extends Subclassed { + Subclass() : super._(); +} + +void s(Subclassed e) { + switch (e) { + case Subclassed.e: + } +} +'''); + } +} + +@reflectiveTest +class ExhaustiveCasesTest extends BaseExhaustiveCasesTest { + test_enum_ok() async { + await assertDiagnostics(actualEnumSource, [ + error(CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_STATEMENT, 52, 6), + ]); + } +} + +@reflectiveTest +class ExhaustiveCasesTestLanguage219 extends BaseExhaustiveCasesTest + with LanguageVersion219Mixin { + test_enum_ok() async { + await assertDiagnostics(actualEnumSource, [ + error(StaticWarningCode.MISSING_ENUM_CONSTANT_IN_SWITCH, 52, 10), + ]); + } +} diff --git a/pkg/linter/test/rules/file_names_test.dart b/pkg/linter/test/rules/file_names_test.dart new file mode 100644 index 00000000000..60c9d414b53 --- /dev/null +++ b/pkg/linter/test/rules/file_names_test.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(FileNamesInvalidTest); + defineReflectiveTests(FileNamesNonStrictTest); + }); +} + +@reflectiveTest +class FileNamesInvalidTest extends LintRuleTest { + @override + String get lintRule => 'file_names'; + + @override + String get testFilePath => '$testPackageLibPath/a-test.dart'; + + test_invalidName() async { + await assertDiagnostics(r''' +class A { } +''', [ + lint(0, 0), + ]); + } +} + +@reflectiveTest +class FileNamesNonStrictTest extends LintRuleTest { + @override + String get lintRule => 'file_names'; + + @override + String get testFilePath => '$testPackageLibPath/non-strict.css.dart'; + + test_validName() async { + await assertNoDiagnostics(r''' +class A { } +'''); + } +} diff --git a/pkg/linter/test/rules/flutter_style_todos_test.dart b/pkg/linter/test/rules/flutter_style_todos_test.dart new file mode 100644 index 00000000000..703888cc7aa --- /dev/null +++ b/pkg/linter/test/rules/flutter_style_todos_test.dart @@ -0,0 +1,164 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(FlutterStyleTodosTest); + }); +} + +@reflectiveTest +class FlutterStyleTodosTest extends LintRuleTest { + @override + String get lintRule => 'flutter_style_todos'; + + // TODO(srawlins): This test is called, "bad patterns", contains 10 TODO-like + // comment lines, but then only expects 9 lints. Why? + test_badPatterns() async { + await assertDiagnostics( + r''' +// TODO something +// Todo something +// todo something +// TODO(somebody) something +// TODO: something +// Todo(somebody): something +// todo(somebody): something +// ToDo(somebody): something +// TODO(somebody): something, github.com/flutter/flutter +// ToDo(somebody): something, https://github.com/flutter/flutter +''', + [ + lint(0, 17), + lint(18, 17), + lint(36, 17), + lint(54, 27), + lint(82, 18), + lint(101, 28), + lint(130, 28), + lint(159, 28), + lint(245, 64), + ], + ); + } + + test_badUsername1() async { + await assertDiagnostics( + r'// TODO(#12357): bla', + [ + lint(0, 20), + ], + ); + } + + test_badUsername2() async { + await assertDiagnostics( + r'// TODO(user1,user2): bla', + [ + lint(0, 25), + ], + ); + } + + test_docComment() async { + await assertDiagnostics( + r'/// TODO(user): bla', + [ + lint(0, 19), + ], + ); + } + + test_extraColon() async { + await assertDiagnostics( + r'// TODO:(user): bla', + [ + lint(0, 19), + ], + ); + } + + test_goodPatterns() async { + await assertNoDiagnostics( + r''' +// TODO(somebody): something +// TODO(somebody): something, https://github.com/flutter/flutter +''', + ); + } + + test_justTodo() async { + await assertDiagnostics( + r'// TODO', + [ + lint(0, 7), + ], + ); + } + + test_justTodo_noLeadingSpace() async { + await assertDiagnostics( + r'//TODO', + [ + lint(0, 6), + ], + ); + } + + test_leadingText() async { + await assertDiagnostics( + r'// comment TODO(user): bla', + [ + lint(0, 26), + ], + ); + } + + test_missingColon() async { + await assertDiagnostics( + r'// TODO(user) bla', + [ + lint(0, 17), + ], + ); + } + + test_missingParens() async { + await assertDiagnostics( + r'// TODO: bla', + [ + lint(0, 12), + ], + ); + } + + test_properFormat_hyphenatedUsername() async { + await assertNoDiagnostics(r'// TODO(user-name): bla'); + } + + test_properFormat_simpleUsername() async { + await assertNoDiagnostics(r'// TODO(username): bla'); + } + + test_slashStar() async { + await assertNoDiagnostics(r'/* TODO bla */'); + } + + test_slashStarStar() async { + await assertNoDiagnostics(r'/** TODO bla **/'); + } + + test_spaceBeforeColon() async { + await assertDiagnostics( + r'// TODO(user) : bla', + [ + lint(0, 19), + ], + ); + } +} diff --git a/pkg/linter/test/rules/hash_and_equals_test.dart b/pkg/linter/test/rules/hash_and_equals_test.dart new file mode 100644 index 00000000000..c68c117f4f7 --- /dev/null +++ b/pkg/linter/test/rules/hash_and_equals_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(HashAndEqualsTest); + }); +} + +@reflectiveTest +class HashAndEqualsTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'hash_and_equals'; + + test_enum_missingHash() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + @override + bool operator ==(Object other) => false; +} +''', [ + error( + CompileTimeErrorCode.ILLEGAL_CONCRETE_ENUM_MEMBER_DECLARATION, 46, 2), + // no lint + ]); + } + + test_extensionType_missingHash() async { + await assertDiagnostics(r''' +extension type E(Object o) { + bool operator ==(Object other) => false; +} +''', [ + // No lint. + error( + CompileTimeErrorCode.EXTENSION_TYPE_DECLARES_MEMBER_OF_OBJECT, 45, 2), + ]); + } +} diff --git a/pkg/linter/test/rules/implicit_reopen_test.dart b/pkg/linter/test/rules/implicit_reopen_test.dart new file mode 100644 index 00000000000..96952644220 --- /dev/null +++ b/pkg/linter/test/rules/implicit_reopen_test.dart @@ -0,0 +1,284 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ImplicitReopenTest); + defineReflectiveTests(ImplicitReopenInducedModifierTest); + }); +} + +@reflectiveTest +class ImplicitReopenInducedModifierTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'implicit_reopen'; + + test_inducedFinal() async { + await assertDiagnostics(r''' +final class F {} +sealed class S extends F {} +base class C extends S {} +''', [ + lint(56, 1), + ]); + } + + test_inducedFinal_base_interface() async { + await assertDiagnostics(r''' +base class C {} +interface class D {} +sealed class E extends D implements C {} +base class B extends E {} +''', [ + lint(89, 1), + ]); + } + + test_inducedFinal_baseMixin_interface() async { + await assertDiagnostics(r''' +interface class D {} +base mixin G {} +sealed class H extends D with G {} +base class B extends H {} +''', [ + lint(83, 1), + ]); + } + + test_inducedFinal_interface_base_ok() async { + await assertDiagnostics(r''' +interface class S {} +base class I {} +sealed class A extends S implements I {} +class C extends A {} +''', [ + // No lint. + error(CompileTimeErrorCode.SUBTYPE_OF_BASE_IS_NOT_BASE_FINAL_OR_SEALED, + 84, 1), + ]); + } + + test_inducedFinal_mixin_finalClass() async { + await assertDiagnostics(r''' +final class S {} +mixin M {} +sealed class A extends S with M {} +base class B extends A {} +''', [ + lint(74, 1), + ]); + } + + test_inducedInterface() async { + await assertDiagnostics(r''' +interface class I {} +sealed class S extends I {} +class C extends S {} +''', [ + lint(55, 1), + ]); + } + + test_inducedInterface_base() async { + await assertDiagnostics(r''' +interface class I {} +sealed class S extends I {} +base class C extends S {} +''', [ + lint(60, 1), + ]); + } + + test_inducedInterface_base_mixin_interface() async { + await assertDiagnostics(r''' +interface class S {} +mixin M {} +sealed class A extends S with M {} +base class C extends A {} +''', [ + lint(78, 1), + ]); + } + + test_inducedInterface_mixin_interface() async { + await assertDiagnostics(r''' +interface class S {} +mixin M {} +sealed class A extends S with M {} +class C extends A {} +''', [ + lint(73, 1), + ]); + } + + test_inducedInterface_twoLevels() async { + await assertDiagnostics(r''' +interface class I {} +sealed class S extends I {} +sealed class S2 extends S {} +class C extends S2 {} +''', [ + lint(84, 1), + ]); + } + + test_subtypingFinalError_ok() async { + await assertDiagnostics(r''' +final class F {} +sealed class S extends F {} +class C extends S {} +''', [ + // No lint. + error(CompileTimeErrorCode.SUBTYPE_OF_FINAL_IS_NOT_BASE_FINAL_OR_SEALED, + 51, 1), + ]); + } +} + +@reflectiveTest +class ImplicitReopenTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'implicit_reopen'; + + test_class_classFinal_ok() async { + await assertDiagnostics(r''' +final class F {} + +class C extends F {} +''', [ + // No lint. + error(CompileTimeErrorCode.SUBTYPE_OF_FINAL_IS_NOT_BASE_FINAL_OR_SEALED, + 24, 1), + ]); + } + + test_class_classInterface() async { + await assertDiagnostics(r''' +interface class I {} + +class C extends I {} +''', [ + lint(28, 1), + ]); + } + + test_class_classInterface_outsideLib_ok() async { + newFile('$testPackageLibPath/a.dart', r''' +interface class I {} +'''); + + await assertDiagnostics(r''' +import 'a.dart'; + +class C extends I {} +''', [ + // No lint. + error(CompileTimeErrorCode.INTERFACE_CLASS_EXTENDED_OUTSIDE_OF_LIBRARY, + 34, 1), + ]); + } + + test_class_classInterface_reopened_ok() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; + +interface class I {} + +@reopen +class C extends I {} +'''); + } + + test_classBase_classFinal() async { + await assertDiagnostics(r''' +final class F {} + +base class B extends F {} +''', [ + lint(29, 1), + ]); + } + + test_classBase_classInterface() async { + await assertDiagnostics(r''' +interface class I {} + +base class B extends I {} +''', [ + lint(33, 1), + ]); + } + + test_classFinal_classInterface_ok() async { + await assertNoDiagnostics(r''' +interface class I {} + +final class C extends I {} +'''); + } + + test_classMixin_classInterface_ok() async { + await assertDiagnostics(r''' +interface class I {} + +mixin class M extends I {} +''', [ + // No lint. + error(CompileTimeErrorCode.MIXIN_CLASS_DECLARATION_EXTENDS_NOT_OBJECT, 44, + 1), + ]); + } + + test_classTypeAlias_class_classInterface() async { + await assertDiagnostics(r''' +interface class I {} +mixin M {} +class C = I with M; +''', [ + lint(38, 1), + ]); + } + + test_classTypeAlias_classBase_classFinal() async { + await assertDiagnostics(r''' +final class C {} +mixin M {} +base class D = C with M; +''', [ + lint(39, 1), + ]); + } + + test_classTypeAlias_classBase_classFinal_reopened() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; + +final class C {} +mixin M {} +@reopen +base class D = C with M; +'''); + } + + test_mixin_classInterface_ok() async { + await assertDiagnostics(r''' +interface class I {} + +mixin M extends I {} +''', [ + // No lint. + error(ParserErrorCode.EXPECTED_INSTEAD, 30, 7), + ]); + } +} diff --git a/pkg/linter/test/rules/invalid_case_patterns_test.dart b/pkg/linter/test/rules/invalid_case_patterns_test.dart new file mode 100644 index 00000000000..78948c35b02 --- /dev/null +++ b/pkg/linter/test/rules/invalid_case_patterns_test.dart @@ -0,0 +1,321 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(InvalidCasePatternsTestLanguage219); + }); +} + +@reflectiveTest +class InvalidCasePatternsTestLanguage219 extends LintRuleTest + with LanguageVersion219Mixin { + @override + String get lintRule => 'invalid_case_patterns'; + + test_binaryExpression_logicalAnd() async { + await assertDiagnostics(r''' +f(bool b) { + switch (b) { + case true && false: + } +} +''', [ + lint(36, 13), + ]); + } + + test_binaryExpression_plus() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case 1 + 2: + } +} +''', [ + lint(43, 5), + ]); + } + + test_conditionalExpression() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case true ? 1 : 2: + } +} +''', [ + lint(43, 12), + error(WarningCode.DEAD_CODE, 54, 1), + ]); + } + + test_constConstructorCall() async { + await assertDiagnostics(r''' +class C { + const C(); +} + +f(C c) { + switch (c) { + case C(): + } +} +''', [ + lint(59, 3), + ]); + } + + test_constConstructorCall_explicitConst_ok() async { + await assertNoDiagnostics(r''' +class C { + const C(); +} +f(C c) { + switch (c) { + case const C(): + } +} +'''); + } + + test_identicalCall() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case identical(1, 2): + } +} +''', [ + lint(43, 15), + ]); + } + + test_isExpression() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case 1 is int: + } +} +''', [ + error(WarningCode.UNNECESSARY_TYPE_CHECK_TRUE, 43, 8), + lint(43, 8), + ]); + } + + test_isNotExpression() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case 1 is! int: + } +} +''', [ + error(WarningCode.UNNECESSARY_TYPE_CHECK_FALSE, 43, 9), + lint(43, 9), + ]); + } + + test_lengthCall() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case ''.length: + } +} +''', [ + lint(43, 9), + ]); + } + + test_listLiteral() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case [1, 2]: + } +} +''', [ + lint(43, 6), + ]); + } + + test_listLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const [1, 2]: + } +} +'''); + } + + test_listLiteral_typeArgs() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case [1, 2]: + } +} +''', [ + lint(43, 11), + ]); + } + + test_mapLiteral() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case {'k': 'v'}: + } +} +''', [ + lint(42, 10), + ]); + } + + test_mapLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const {'k': 'v'}: + } +} +'''); + } + + test_mapLiteral_parenthesized() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case ({'k': 'v'}): + } +} +''', [ + lint(43, 10), + ]); + } + + test_mapLiteral_parenthesized_twice() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case (({'k': 'v'})): + } +} +''', [ + lint(44, 10), + ]); + } + + test_mapLiteral_typeArgs() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case {'k': 'v'}: + } +} +''', [ + lint(42, 25), + ]); + } + + test_parenthesizedExpression_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case (1): + } +} +'''); + } + + test_prefixedExpression_intLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case -1: + } +} +'''); + } + + test_setLiteral() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case {1}: + } +} +''', [ + lint(43, 3), + ]); + } + + test_setLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const {1}: + } +} +'''); + } + + test_setLiteral_typeArgs() async { + await assertDiagnostics(r''' +void f(Object o) { + switch (o) { + case {1}: + } +} +''', [ + lint(43, 8), + ]); + } + + test_unaryOperator_minus() async { + await assertDiagnostics(r''' +void f() { + const o = 1; + switch (1) { + case -o: + } +} +''', [ + lint(50, 2), + ]); + } + + test_unaryOperator_not() async { + await assertDiagnostics(r''' + void f() { + const b = false; + switch (true) { + case !b: + } + } +''', [ + lint(65, 2), + ]); + } + + test_wildcard() async { + await assertDiagnostics(r''' +f(int n) { + const _ = 3; + switch (n) { + case _: + } +} +''', [ + lint(50, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/join_return_with_assignment_test.dart b/pkg/linter/test/rules/join_return_with_assignment_test.dart new file mode 100644 index 00000000000..93f80c6ea8c --- /dev/null +++ b/pkg/linter/test/rules/join_return_with_assignment_test.dart @@ -0,0 +1,214 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/rules/join_return_with_assignment.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(JoinReturnWithAssignmentTest); + }); +} + +@reflectiveTest +class JoinReturnWithAssignmentTest extends LintRuleTest { + @override + String get lintRule => JoinReturnWithAssignment.code.name; + + test_class_field_propertyAccess() async { + await assertDiagnostics(r''' +class A { + int f = 0; +} + +int f(A a) { + a.f = 0; + return a.f; +} +''', [ + lint(41, 8), + ]); + } + + test_class_field_propertyAccess_nested() async { + await assertDiagnostics(r''' +class A { + int f = 0; +} + +class B { + A a = A(); +} + +int f(B b) { + b.a.f = 0; + return b.a.f; +} +''', [ + lint(67, 10), + ]); + } + + test_class_field_propertyAccess_nested_notSame() async { + await assertNoDiagnostics(r''' +class A { + int f = 0; +} + +class B { + A a = A(); +} + +int f(B b1, B b2) { + b1.a.f = 0; + return b2.a.f; +} +'''); + } + + test_class_field_propertyAccess_notSame() async { + await assertNoDiagnostics(r''' +class A { + int f = 0; +} + +int f(A a1, A a2) { + a1.f = 0; + return a2.f; +} +'''); + } + + test_class_field_withoutPrefix() async { + await assertDiagnostics(r''' +class A { + int? _a; + int? foo() { + _a ??= 0; + return _a; + } +} +''', [ + lint(40, 9), + ]); + } + + test_class_field_withoutPrefix_ifThenElse() async { + await assertDiagnostics(r''' +class A { + int? _a; + int? foo(bool b) { + if (b) { + _a = 0; + return _a; + } else { + _a = 1; + return _a; + } + } +} +''', [ + lint(61, 7), + lint(105, 7), + ]); + } + + test_localVariable_assignment() async { + await assertDiagnostics(r''' +int f(int a) { + a = 0; + return a; +} +''', [ + lint(17, 6), + ]); + } + + test_localVariable_assignment_compound() async { + await assertDiagnostics(r''' +int f(int a) { + a += 0; + return a; +} +''', [ + lint(17, 7), + ]); + } + + @failingTest + test_localVariable_assignment_multiple() async { + await assertNoDiagnostics(r''' +int f1(int a, int b) { + a = b = 0; + return a; +} + +int f2(int a, int b) { + a = b = 0; + return b; +} +'''); + } + + test_localVariable_declaration() async { + await assertNoDiagnostics(r''' +int f(int a) { + var a = 0; + return a; +} +'''); + } + + test_localVariable_hasPreviousAssignment() async { + await assertNoDiagnostics(r''' +int f(int a) { + a += 1; + a += 2; + return a; +} +'''); + } + + test_localVariable_postfix() async { + await assertDiagnostics(r''' +int f(int a) { + a++; + return a; +} +''', [ + lint(17, 4), + ]); + } + + test_localVariable_prefix() async { + await assertDiagnostics(r''' +int f(int a) { + ++a; + return a; +} +''', [ + lint(17, 4), + ]); + } + + test_patternAssignment_multiple() async { + await assertNoDiagnostics(r''' +int f(int a, int b) { + (a, b) = (0, 1); + return a; +} +'''); + } + + test_patternVariableDeclaration_multiple() async { + await assertNoDiagnostics(r''' +int f() { + var (a, b) = (0, 1); + return a; +} +'''); + } +} diff --git a/pkg/linter/test/rules/leading_newlines_in_multiline_strings_test.dart b/pkg/linter/test/rules/leading_newlines_in_multiline_strings_test.dart new file mode 100644 index 00000000000..7070fad2d86 --- /dev/null +++ b/pkg/linter/test/rules/leading_newlines_in_multiline_strings_test.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LeadingNewlinesInMultilineStringsTest); + }); +} + +@reflectiveTest +class LeadingNewlinesInMultilineStringsTest extends LintRuleTest { + @override + String get lintRule => 'leading_newlines_in_multiline_strings'; + + test_emptyString() async { + await assertNoDiagnostics(r""" +var x = ''''''; +"""); + } + + test_leadingNewline() async { + await assertNoDiagnostics(r""" +var x = ''' +this is a multiline string'''; +"""); + } + + test_leadingNewline_withInterpolation() async { + await assertNoDiagnostics(r""" +var a = 'a'; +var x = ''' +this is a multiline string $a'''; +"""); + } + + test_noNewline() async { + await assertNoDiagnostics(r""" +var x = '''this is a multiline string'''; +"""); + } + + test_noNewline_doubleQuotes() async { + await assertNoDiagnostics(r''' +var x = """uses double quotes"""; +'''); + } + + test_noNewline_interpolation() async { + await assertNoDiagnostics(r""" +var a = 'a'; +var x = '''$a'''; +"""); + } + + test_noNewline_withInterpolation() async { + await assertNoDiagnostics(r""" +var a = 'a'; +var x = '''this is a multiline string $a'''; +"""); + } + + test_textBeforeNewline() async { + await assertDiagnostics(r""" +var x = '''this + is a multiline string'''; +""", [ + lint(8, 33), + ]); + } + + test_textBeforeNewline_withInterpolation() async { + await assertDiagnostics(r""" +var a = 'a'; +var x = '''this + is a multiline string$a'''; +""", [ + lint(21, 35), + ]); + } +} diff --git a/pkg/linter/test/rules/library_annotations_test.dart b/pkg/linter/test/rules/library_annotations_test.dart new file mode 100644 index 00000000000..b9647b6ea35 --- /dev/null +++ b/pkg/linter/test/rules/library_annotations_test.dart @@ -0,0 +1,160 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LibraryAnnotationsTest); + }); +} + +@reflectiveTest +class LibraryAnnotationsTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'library_annotations'; + + test_classDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +class C {} +''', + [lint(0, 29)], + ); + } + + test_classTypeAliasDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +abstract class C = Object with M; + +mixin M {} +''', + [lint(0, 29)], + ); + } + + test_enumDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +enum E { one, two } +''', + [lint(0, 29)], + ); + } + + test_exportDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +export 'dart:math'; +''', + [lint(0, 29)], + ); + } + + test_extensionDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +extension E on int {} +''', + [lint(0, 29)], + ); + } + + test_functionDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +void f() {} +''', + [lint(0, 29)], + ); + } + + test_functionDeclaration_annotationWithTargetKindLibrary() async { + // In this library, `invalid_annotation_target` is reported (and + // suppressed), so we do not also report `library_annotations`. + await assertNoDiagnostics( + r''' +import 'package:meta/meta_meta.dart'; + +@Target({TargetKind.library}) +class TestOn { + const TestOn(String name); +} + +// ignore: invalid_annotation_target +@TestOn('browser') +class C {} +''', + ); + } + + test_genericTypedefDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +typedef Fn = void Function(); +''', + [lint(0, 29)], + ); + } + + test_importDirective_annotationWithTargetKindLibrary() async { + await assertDiagnostics( + r''' +@TestOn('browser') +import 'package:meta/meta_meta.dart'; + +@Target({TargetKind.library}) +class TestOn { + const TestOn(String name); +} + +class C {} +''', + [lint(0, 18)], + ); + } + + test_mixinDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +mixin M {} +''', + [lint(0, 29)], + ); + } + + test_topLevelVariableDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +var i = 1; +''', + [lint(0, 29)], + ); + } + + test_typedefDeclaration() async { + await assertDiagnostics( + r''' +@pragma('dart2js:late:trust') +typedef void Fn(); +''', + [lint(0, 29)], + ); + } +} diff --git a/pkg/linter/test/rules/library_names_test.dart b/pkg/linter/test/rules/library_names_test.dart new file mode 100644 index 00000000000..5f263beca0e --- /dev/null +++ b/pkg/linter/test/rules/library_names_test.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LibraryNamesTest); + }); +} + +@reflectiveTest +class LibraryNamesTest extends LintRuleTest { + @override + String get lintRule => 'library_names'; + + test_libraryWithoutName() async { + await assertNoDiagnostics(''' +library; +'''); + } + + test_lowercase() async { + await assertNoDiagnostics(''' +library foo; +'''); + } + + test_noLibrary() async { + await assertNoDiagnostics(''' +'''); + } + + test_titlecase() async { + await assertDiagnostics(''' +library Foo; +''', [ + lint(8, 3), + ]); + } + + test_uppercaseInDots() async { + await assertDiagnostics(''' +library one.Two.three; +''', [ + lint(8, 13), + ]); + } +} diff --git a/pkg/linter/test/rules/library_private_types_in_public_api_test.dart b/pkg/linter/test/rules/library_private_types_in_public_api_test.dart new file mode 100644 index 00000000000..3f9cc19c2f0 --- /dev/null +++ b/pkg/linter/test/rules/library_private_types_in_public_api_test.dart @@ -0,0 +1,271 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LibraryPrivateTypesInPublicApiEnumTest); + defineReflectiveTests(LibraryPrivateTypesInPublicApiExtensionTypeTest); + defineReflectiveTests(LibraryPrivateTypesInPublicApiSuperParamTest); + }); +} + +@reflectiveTest +class LibraryPrivateTypesInPublicApiEnumTest extends LintRuleTest { + @override + String get lintRule => 'library_private_types_in_public_api'; + + test_abstractFinal_constructorParams() async { + await assertNoDiagnostics(r''' +class _O { + const _O(); +} + +abstract final class E { + E(_O o); +} +'''); + } + + test_abstractInterface_constructorParams() async { + await assertNoDiagnostics(r''' +class _O { + const _O(); +} + +abstract interface class E { + E(_O o); +} +'''); + } + + test_enum() async { + await assertDiagnostics(r''' +class _O {} +enum E { + a, b, c; + final _O o = _O(); + void oo(_O o) { } + _O get ooo => o; +} +''', [ + lint(40, 2), + lint(63, 2), + lint(75, 2), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4470 + test_enum_constructorParams() async { + await assertNoDiagnostics(r''' +class _O { + const _O(); +} +enum E { + a(_O()); + const E(_O o); +} +'''); + } + + test_sealed_constructorParams() async { + await assertNoDiagnostics(r''' +class _O { + const _O(); +} + +sealed class E { + E(_O o); +} +'''); + } +} + +@reflectiveTest +class LibraryPrivateTypesInPublicApiExtensionTypeTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'library_private_types_in_public_api'; + + test_constructorParam() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + E.e(_C c) : o = c; +} +''', [ + lint(47, 2), + ]); + } + + test_extensionTypeDeclaration_representation() async { + await assertDiagnostics(r''' +class _C {} +extension type E(_C c) {} +''', [ + lint(29, 2), + ]); + } + + test_extensionTypeDeclaration_representation_private() async { + await assertNoDiagnostics(r''' +class _C {} +extension type E(_C _c) {} +'''); + } + + test_extensionTypeDeclaration_typeParam() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) {} +''', [ + lint(39, 2), + ]); + } + + test_field_instance() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + _C? c; +} +''', [ + // No lint. + error(CompileTimeErrorCode.EXTENSION_TYPE_DECLARES_INSTANCE_FIELD, 47, 1), + ]); + } + + test_field_static() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + static _C? c; +} +''', [ + lint(50, 2), + ]); + } + + test_method_instance_param() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + m(_C c){} +} +''', [ + lint(45, 2), + ]); + } + + test_method_instance_private_param() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + _m(_C c){} +} +''', [ + error(WarningCode.UNUSED_ELEMENT, 43, 2), + ]); + } + + test_method_instance_returnType() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + _C? m() => null; +} +''', [ + lint(43, 2), + ]); + } + + test_method_static_param() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + static m(_C c){} +} +''', [ + lint(52, 2), + ]); + } + + test_method_static_private_returnType() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + static _C? _m() => null; +} +''', [ + error(WarningCode.UNUSED_ELEMENT, 54, 2), + ]); + } + + test_method_static_returnType() async { + await assertDiagnostics(r''' +class _C {} +extension type E(Object o) { + static _C? m() => null; +} +''', [ + lint(50, 2), + ]); + } +} + +@reflectiveTest +class LibraryPrivateTypesInPublicApiSuperParamTest extends LintRuleTest { + @override + String get lintRule => 'library_private_types_in_public_api'; + + test_implicitTypeFieldFormalParam() async { + await assertDiagnostics(r''' +class _O {} +class C { + _O _x; + + C(this._x); + + Object get x => _x; +} +''', [ + lint(41, 2), + ]); + } + + test_implicitTypeSuperFormalParam() async { + await assertDiagnostics(r''' +class _O extends Object {} +class _A { + _A(_O o); +} +class B extends _A { + B(super.o); +} +''', [ + lint(83, 1), + ]); + } + + test_recursiveInterfaceInheritance() async { + await assertDiagnostics(r''' +class _O extends Object {} +class A { + Object o; + A(this.o); +} + +class B extends A { + B(_O super.o); +} +''', [ + lint(89, 2), + ]); + } +} diff --git a/pkg/linter/test/rules/lines_longer_than_80_chars_test.dart b/pkg/linter/test/rules/lines_longer_than_80_chars_test.dart new file mode 100644 index 00000000000..688b1e4102b --- /dev/null +++ b/pkg/linter/test/rules/lines_longer_than_80_chars_test.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LinesLongerThan80CharsTest); + }); +} + +@reflectiveTest +class LinesLongerThan80CharsTest extends LintRuleTest { + @override + String get lintRule => 'lines_longer_than_80_chars'; + + test_endOfLineComment_shorterThan80() async { + await assertNoDiagnostics( + '// 5 10 15 20 25 30 35 40 45 50', + ); + } + + test_endOfLineComment_spaceAfter80() async { + await assertDiagnostics( + '// 5 10 15 20 25 30 35 40 50 55 60' + ' 65 70 75 80 85 90 95 100', + [ + lint(80, 15), + ], + ); + } + + test_endOfLineComment_noSpaceAfter80() async { + await assertNoDiagnostics( + '// 5 10 15 20 25 30 35 40 50 55 60 ' + 'http://url.com/abcde/abcde/abcde/abcde.dart', + ); + } + + test_docComment_shorterThan80() async { + await assertNoDiagnostics( + '/// 5 10 15 20 25 30 35 40 45 50', + ); + } + + test_docComment_spaceAfter80() async { + await assertDiagnostics( + '/// 5 10 15 20 25 30 35 40 50 55 60' + ' 65 70 75 80 85 90 95 100', + [ + lint(80, 15), + ], + ); + } + + test_docComment_noSpaceAfter80() async { + await assertNoDiagnostics( + '/// 5 10 15 20 25 30 35 40 50 55 60 ' + 'http://url.com/abcde/abcde/abcde/abcde.dart', + ); + } + + test_blockComment_shorterThan80() async { + await assertNoDiagnostics( + '/* 5 10 15 20 25 30 35 40 45 50 */', + ); + } + + test_blockComment_spaceAfter80() async { + await assertDiagnostics( + '/* 5 10 15 20 25 30 35 40 50 55 60' + ' 65 70 75 80 85 90 95 100 */', + [ + lint(80, 18), + ], + ); + } + + test_blockComment_noSpaceAfter80() async { + await assertNoDiagnostics( + '/* 5 10 15 20 25 30 35 40 50 55 60 ' + 'http://url.com/abcde/abcde/abcde/abcde.dart */', + ); + } + + test_multilineBlockComment_shorterThan80() async { + await assertNoDiagnostics( + '/*\n' + ' * 5 10 15 20 25 30 35 40 45 50\n' + ' */', + ); + } + + test_multilineBlockComment_shorterThan80_withCrlf() async { + await assertNoDiagnostics( + '/*\n' + ' * 5 10 15 20 25 30 35 40 45 50\r\n' + ' * 5 10 15 20 25 30 35 40 45 50\r\n' + ' */', + ); + } + + test_multilineBlockComment_spaceAfter80() async { + await assertDiagnostics( + '/*\n' + ' * 5 10 15 20 25 30 35 40 50 55 60' + ' 65 70 75 80 85 90 95 100\n' + ' */', + [ + lint(83, 15), + ], + ); + } + + test_multilineBlockComment_noSpaceAfter80() async { + await assertNoDiagnostics( + '/*\n' + ' * 5 10 15 20 25 30 35 40 50 55 60 ' + 'http://url.com/abcde/abcde/abcde/abcde.dart\n' + ' */', + ); + } +} diff --git a/pkg/linter/test/rules/literal_only_boolean_expressions_test.dart b/pkg/linter/test/rules/literal_only_boolean_expressions_test.dart new file mode 100644 index 00000000000..eb162d437f7 --- /dev/null +++ b/pkg/linter/test/rules/literal_only_boolean_expressions_test.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(LiteralOnlyBooleanExpressionsTest); + }); +} + +@reflectiveTest +class LiteralOnlyBooleanExpressionsTest extends LintRuleTest { + @override + String get lintRule => 'literal_only_boolean_expressions'; + + test_ifCase_intLiteral() async { + await assertDiagnostics(r''' +void f() { + if (1 case {1:0}) { + print(''); + } +} +''', [ + // No lint + error(WarningCode.PATTERN_NEVER_MATCHES_VALUE_TYPE, 24, 5), + ]); + } + + test_ifCase_listLiteral() async { + await assertNoDiagnostics(r''' +void f() { + if ([1] case [1]) { + print(''); + } +} +'''); + } + + test_ifCase_mapLiteral() async { + await assertNoDiagnostics(r''' +void f() { + if ({1:0} case {1:0}) { + print(''); + } +} +'''); + } + + test_ifCase_objectInstantiation() async { + await assertNoDiagnostics(r''' +class A { + final int a; + A(this.a); +} + +void f() { + if (A(1) case A(a: 1)) { + print(''); + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4352 + test_ifCase_record() async { + await assertNoDiagnostics(r''' +void f() { + if (('', '') case (String(), String())) { + print(''); + } +} +'''); + } + + test_switchExpression() async { + await assertNoDiagnostics(r''' +bool f(Object o) => switch(o) { + [1] => true, + {1:1} => false, + (1, 1) => false, + String(isEmpty: false) => false, + _ => false, + }; +'''); + } + + test_whenClause() async { + await assertDiagnostics(r''' +void f() { + switch (1) { + case [int a] when true: print(a); + } +} +''', [ + error(WarningCode.PATTERN_NEVER_MATCHES_VALUE_TYPE, 35, 7), + lint(43, 9), + ]); + } + + test_whileTrue() async { + await assertNoDiagnostics(r''' +void f() { + while (true) { + print('!'); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/matching_super_parameters_test.dart b/pkg/linter/test/rules/matching_super_parameters_test.dart new file mode 100644 index 00000000000..dc3c0e60656 --- /dev/null +++ b/pkg/linter/test/rules/matching_super_parameters_test.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(MatchingSuperParametersTest); + }); +} + +@reflectiveTest +class MatchingSuperParametersTest extends LintRuleTest { + @override + String get lintRule => 'matching_super_parameters'; + + test_explicitSuperInvocation_matchingWithOffset() async { + await assertNoDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.x, + super.y, + ) : super.named(); +} + +class C { + final int x; + final int y; + + C.named(this.x, this.y); +} +'''); + } + + test_explicitSuperInvocation_matchingWithOffset_withNamedSuper() async { + await assertNoDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.x, + super.y, { + required super.z, + }) : super.named(); +} + +class C { + final int x; + final int y; + final int z; + + C.named(this.x, this.y, {required this.z}); +} +'''); + } + + test_explicitSuperInvocation_matchingWithOffset_withNamedExplicit() async { + await assertNoDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.x, + super.y, + ) : super.named(z: 1); +} + +class C { + final int x; + final int y; + final int z; + + C.named(this.x, this.y, {required this.z}); +} +'''); + } + + test_explicitSuperInvocation_nonMatching() async { + await assertDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.y, + super.x, + ) : super.named(); +} + +class C { + final int x; + final int y; + + C.named(this.x, this.y); +} +''', [ + lint(62, 7), + lint(75, 7), + ]); + } + + test_implicitSuperInvocation_matchingWithGap() async { + await assertNoDiagnostics(r''' +class D extends C { + final String w; + + D( + super.x, + this.w, + super.y, + ); +} + +class C { + final int x; + final int y; + + C(this.x, this.y); +} +'''); + } + + test_implicitSuperInvocation_matchingWithOffset() async { + await assertNoDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.x, + super.y, + ); +} + +class C { + final int x; + final int y; + + C(this.x, this.y); +} +'''); + } + + test_implicitSuperInvocation_nonMatching() async { + await assertDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.y, + super.x, + ); +} + +class C { + final int x; + final int y; + + C(this.x, this.y); +} +''', [ + lint(62, 7), + lint(75, 7), + ]); + } + + test_implicitSuperInvocation_nonMatching_omittedOptional() async { + await assertDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.y, + ); +} + +class C { + final int x; + final int? y; + + C(this.x, [this.y]); +} +''', [ + lint(62, 7), + ]); + } + + test_implicitSuperInvocation_nonMatching_tooMany() async { + await assertDiagnostics(r''' +class D extends C { + final String w; + + D( + this.w, + super.y, + super.x, + ); +} + +class C { + final int x; + + C(this.x); +} +''', [ + // No lint. + error( + CompileTimeErrorCode + .SUPER_FORMAL_PARAMETER_WITHOUT_ASSOCIATED_POSITIONAL, + 81, + 1), + ]); + } +} diff --git a/pkg/linter/test/rules/missing_whitespace_between_adjacent_strings_test.dart b/pkg/linter/test/rules/missing_whitespace_between_adjacent_strings_test.dart new file mode 100644 index 00000000000..bb8d6417d28 --- /dev/null +++ b/pkg/linter/test/rules/missing_whitespace_between_adjacent_strings_test.dart @@ -0,0 +1,136 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(MissingWhitespaceBetweenAdjacentStringsTest); + }); +} + +@reflectiveTest +class MissingWhitespaceBetweenAdjacentStringsTest extends LintRuleTest { + @override + String get lintRule => 'missing_whitespace_between_adjacent_strings'; + + test_argumentToMatches() async { + await assertNoDiagnostics(r''' +var x = matches('(\n)+' '(\n)+' '(\n)+'); +void matches(String s) {} +'''); + } + + test_argumentToRegExpConstructor() async { + await assertNoDiagnostics(r''' +var x = RegExp('(\n)+' '(\n)+' '(\n)+'); +'''); + } + + test_extraPositionalArgument() async { + await assertDiagnostics(r''' +void f() { + new Unresolved('aaa' 'bbb'); +} +''', [ + // No lint + error(CompileTimeErrorCode.NEW_WITH_NON_TYPE, 17, 10), + ]); + } + + test_firstPartEndsWithCarriageReturn() async { + await assertNoDiagnostics(r''' +var x= 'long line\r' 'is long'; +'''); + } + + test_firstPartEndsWithSpace() async { + await assertNoDiagnostics(r''' +var x = 'long line ' 'is long'; +'''); + } + + test_firstPartEndsWithTab() async { + await assertNoDiagnostics(r''' +var x = 'long line\t' 'is long'; +'''); + } + + test_leftPartEndsWithInteroplation() async { + await assertNoDiagnostics(r''' +var f = 1; +var x = 'a $f' 'b'; +'''); + } + + test_leftPartEndsWithInterpolation() async { + await assertNoDiagnostics(r''' +var x = '${1 == 2 ? 'Hello ' : ''}' 'world'; +'''); + } + + test_leftPartEndsWithNewline() async { + await assertNoDiagnostics(r''' +var x = 'long line\n' 'is long'; +'''); + } + + test_leftPartEndsWithOpenParenthesis() async { + await assertNoDiagnostics(r''' +var x = '${1 + 1}(' 'long line)'; +'''); + } + + test_leftPartEndsWithSpace_leftPartHasInterpolation() async { + await assertNoDiagnostics(r''' +var f = 1; +var x = 'long $f line ' 'is long'; +'''); + } + + test_leftPartHasNoSpaces() async { + await assertNoDiagnostics(r''' +var x = 'longLineWithoutSpaceCouldBe' 'AnURL'; +'''); + } + + test_noSpacesBetweenStringParts() async { + await assertDiagnostics(r''' +var x = 'long line' 'is long'; +''', [ + lint(8, 11), + ]); + } + + test_noSpacesBetweenStringParts_leftHasInterpolation() async { + await assertDiagnostics(r''' +var f = 1; +var x = 'long $f line' 'is long'; +''', [ + lint(19, 14), + ]); + } + + test_rightPartStartsWithInterpolation() async { + await assertNoDiagnostics(r''' +var f = 1; +var x = 'a' '$f b'; +'''); + } + + test_secondPartStartsWithSpace() async { + await assertNoDiagnostics(r''' +var x = 'long line' ' is long'; +'''); + } + + test_secondPartStartsWithSpace_eachHasInterpolation_doubleQuotes() async { + await assertNoDiagnostics(r''' +var f = 1; +var x = "a $f b" " c$f"; +'''); + } +} diff --git a/pkg/linter/test/rules/no_adjacent_strings_in_list_test.dart b/pkg/linter/test/rules/no_adjacent_strings_in_list_test.dart new file mode 100644 index 00000000000..a71a4a652bc --- /dev/null +++ b/pkg/linter/test/rules/no_adjacent_strings_in_list_test.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoAdjacentStringsInListTest); + }); +} + +@reflectiveTest +class NoAdjacentStringsInListTest extends LintRuleTest { + @override + String get lintRule => 'no_adjacent_strings_in_list'; + + test_listLiteral() async { + await assertDiagnostics(r''' +var list = [ + 'a' + 'b', + 'c', +]; +''', [ + lint(15, 9), + ]); + } + + test_adjacentStrings_three() async { + await assertDiagnostics(r''' +var list = [ + 'a' + 'b' + 'c' +]; +''', [ + lint(15, 15), + ]); + } + + test_ifElement() async { + await assertDiagnostics(r''' +var list = [ + if (1 == 2) 'a' + 'b' +]; +''', [ + lint(27, 9), + ]); + } + + test_ifElementWithElse_inThen() async { + await assertNoDiagnostics(r''' +var list = [ + if (1 == 2) 'a' + 'b' + else 'c' +]; +'''); + } + + test_ifElementWithElse_inElse() async { + await assertDiagnostics(r''' +var list = [ + if (1 == 2) 'a' + else 'b' 'c' +]; +''', [ + lint(38, 7), + ]); + } + + test_forElement() async { + await assertDiagnostics(r''' +var list = [ + for (var v in []) 'a' + 'b' +]; +''', [ + lint(33, 9), + ]); + } + + test_setLiteral() async { + await assertDiagnostics(r''' +var set = { + 'a' + 'b', + 'c', +}; +''', [ + lint(14, 9), + ]); + } + + test_listLiteral_plusOperator() async { + await assertNoDiagnostics(r''' + var list = [ + 'a' + + 'b', + 'c', + ]; +'''); + } + + test_setLiteral_plusOperator() async { + await assertNoDiagnostics(r''' +var set = { + 'a' + + 'b', + 'c', +}; +'''); + } + + test_switchPattern() async { + await assertDiagnostics(r''' +void f() { + List row = []; + switch (row) { + case ['one' 'two', var name!]: + } +} +''', [ + lint(64, 11), + ]); + } +} diff --git a/pkg/linter/test/rules/no_duplicate_case_values_test.dart b/pkg/linter/test/rules/no_duplicate_case_values_test.dart new file mode 100644 index 00000000000..d1a2bffd31a --- /dev/null +++ b/pkg/linter/test/rules/no_duplicate_case_values_test.dart @@ -0,0 +1,209 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoDuplicateCaseValuesTestLanguage219); + defineReflectiveTests(NoDuplicateCaseValuesTest); + }); +} + +abstract class BaseNoDuplicateCaseValuesTest extends LintRuleTest { + @override + String get lintRule => 'no_duplicate_case_values'; +} + +@reflectiveTest +class NoDuplicateCaseValuesTest extends BaseNoDuplicateCaseValuesTest { + test_duplicateConstClassValue_ok() async { + await assertDiagnostics(r''' +class ConstClass { + final int v; + const ConstClass(this.v); +} + +void switchConstClass() { + ConstClass v = new ConstClass(1); + + switch (v) { + case const ConstClass(1): + case const ConstClass(2): + case const ConstClass(3): + case const ConstClass(2): + default: + } +} +''', [ + error(WarningCode.UNREACHABLE_SWITCH_CASE, 239, 4), + ]); + } + + test_duplicateEnumValue_ok() async { + await assertDiagnostics(r''' +enum E { + one, + two, + three +} + +void switchEnum() { + E v = E.one; + + switch (v) { + case E.one: + case E.two: + case E.three: + case E.two: + default: + } +} +''', [ + // No lint. + error(WarningCode.UNREACHABLE_SWITCH_CASE, 144, 4), + ]); + } + + test_duplicateIntConstant_ok() async { + await assertDiagnostics(r''' +void switchInt() { + const int A = 1; + int v = 5; + + switch (v) { + case 1: + case 2: + case A: + case 2: + case 3: + default: + } +} +''', [ + // No lint. + error(WarningCode.UNREACHABLE_SWITCH_CASE, 95, 4), + error(WarningCode.UNREACHABLE_SWITCH_CASE, 107, 4), + ]); + } + + test_duplicateStringConstant_ok() async { + await assertDiagnostics(r''' +void switchString() { + const String A = 'a'; + String v = 'aa'; + + switch (v) { + case 'aa': + case 'bb': + case A + A: + case 'bb': + case A + 'b': + default: + } +} +''', [ + // No lint. + error(WarningCode.UNREACHABLE_SWITCH_CASE, 115, 4), + error(ParserErrorCode.INVALID_CONSTANT_PATTERN_BINARY, 122, 1), + error(WarningCode.UNREACHABLE_SWITCH_CASE, 131, 4), + error(ParserErrorCode.INVALID_CONSTANT_PATTERN_BINARY, 153, 1), + ]); + } +} + +@reflectiveTest +class NoDuplicateCaseValuesTestLanguage219 extends BaseNoDuplicateCaseValuesTest + with LanguageVersion219Mixin { + test_duplicateConstClassValue() async { + await assertDiagnostics(r''' +class ConstClass { + final int v; + const ConstClass(this.v); +} + +void switchConstClass() { + ConstClass v = new ConstClass(1); + + switch (v) { + case const ConstClass(1): + case const ConstClass(2): + case const ConstClass(3): + case const ConstClass(2): + default: + } +} +''', [ + lint(244, 19), + ]); + } + + test_duplicateEnumValue() async { + await assertDiagnostics(r''' +enum E { + one, + two, + three +} + +void switchEnum() { + E v = E.one; + + switch (v) { + case E.one: + case E.two: + case E.three: + case E.two: + default: + } +} +''', [ + lint(149, 5), + ]); + } + + test_duplicateIntConstant() async { + await assertDiagnostics(r''' +void switchInt() { + const int A = 1; + int v = 5; + + switch (v) { + case 1: + case 2: + case A: + case 2: + case 3: + default: + } +} +''', [ + lint(100, 1), + lint(112, 1), + ]); + } + + test_duplicateStringConstant() async { + await assertDiagnostics(r''' +void switchString() { + const String A = 'a'; + String v = 'aa'; + + switch (v) { + case 'aa': + case 'bb': + case A + A: + case 'bb': + case A + 'b': + default: + } +} +''', [ + lint(120, 5), + lint(136, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/no_leading_underscores_for_local_identifiers_test.dart b/pkg/linter/test/rules/no_leading_underscores_for_local_identifiers_test.dart new file mode 100644 index 00000000000..4b93dd7ac59 --- /dev/null +++ b/pkg/linter/test/rules/no_leading_underscores_for_local_identifiers_test.dart @@ -0,0 +1,213 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoLeadingUnderscoresForLocalIdentifiersTest); + }); +} + +@reflectiveTest +class NoLeadingUnderscoresForLocalIdentifiersTest extends LintRuleTest { + @override + String get lintRule => 'no_leading_underscores_for_local_identifiers'; + + test_listPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case [int _x, int y]) {} +} +''', [ + lint(32, 2), + ]); + } + + test_listPattern_switch() async { + await assertDiagnostics(r''' +f() { + switch ([1,2]) { + case [1 && var _a, 2 && var b]: + } +} +''', [ + lint(44, 2), + ]); + } + + test_listPattern_switch_leftOperand() async { + await assertDiagnostics(r''' +f() { + switch ([1,2]) { + case [var _a && 1, 2 && var b]: + } +} +''', [ + lint(39, 2), + ]); + } + + test_mapPattern_destructured() async { + await assertDiagnostics(r''' +f() { + final {'first': _a, 'second': b} = {'first': 1, 'second': 2}; +} +''', [ + lint(24, 2), + ]); + } + + test_mapPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case {'x': var _x, 'y' : var y}) {} +} +''', [ + lint(37, 2), + ]); + } + + test_mapPattern_switch() async { + await assertDiagnostics(r''' +f() { + switch ({1: 2}) { + case {'a': var _a, 'b': var b} : + } +} +''', [ + lint(45, 2), + ]); + } + + test_objectPattern_destructured() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + final A(a: _b) = A(1); +} +''', [ + lint(53, 2), + ]); + } + + test_objectPattern_destructured_field() async { + await assertNoDiagnostics(r''' +class A { + int _a; + A(this._a); +} +f() { + var A(:_a) = A(1); +} +'''); + } + + test_objectPattern_ifCase() async { + await assertDiagnostics(r''' +class C { + int c; + int d; + C(this.c, this.d); +} + +f(Object o) { + if (o case C(c: var _x, d: var y)) {} +} +''', [ + lint(88, 2), + ]); + } + + test_objectPattern_switch() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + switch (A(1)) { + case A(a: >0 && var _b): + } +} +''', [ + lint(82, 2), + ]); + } + + test_objectPattern_switch_field() async { + await assertNoDiagnostics(r''' +class A { + var _a; +} + +f(A a) { + switch (a) { + case A(:var _a): + } + switch (a) { + case A(:var _a?): + case A(:var _a!): + } +} +'''); + } + + test_recordPattern_destructured() async { + await assertDiagnostics(r''' +f() { + var (_a, b) = ('a', 'b'); +} +''', [ + lint(13, 2), + ]); + } + + test_recordPattern_destructured_field() async { + await assertDiagnostics(r''' +f() { + var (a: _a, :b) = (a: 1, b: 1); +} +''', [ + lint(16, 2), + ]); + } + + test_recordPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case (int _x, int y)) {} +} +''', [ + lint(32, 2), + ]); + } + + test_recordPattern_ifCase_field() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case (x: int _x, :int y)) {} +} +''', [ + lint(35, 2), + ]); + } + + test_recordPattern_switch() async { + await assertDiagnostics(r''' +f() { + switch ((1, 2)) { + case (var _a, var b): + } +} +''', [ + lint(40, 2), + ]); + } +} diff --git a/pkg/linter/test/rules/no_logic_in_create_state_test.dart b/pkg/linter/test/rules/no_logic_in_create_state_test.dart new file mode 100644 index 00000000000..8f8927ddb92 --- /dev/null +++ b/pkg/linter/test/rules/no_logic_in_create_state_test.dart @@ -0,0 +1,165 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoLogicInCreateStateTest); + }); +} + +@reflectiveTest +class NoLogicInCreateStateTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'no_logic_in_create_state'; + + test_abstract() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatefulWidget { + @override + MyState createState(); +} + +class MyState extends State { + int field = 0; +} +'''); + } + + test_arrowBody_returnsState() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + @override + MyState createState() => MyState(); +} + +class MyState extends State { + int field = 0; +} +'''); + } + + test_arrowBody_returnsState_passingArguments() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + @override + MyState createState() => MyState(1); +} + +class MyState extends State { + int field; + MyState(this.field); +} +''', [ + lint(119, 10), + ]); + } + + test_arrowBody_returnsState_withCascade() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + @override + MyState createState() => MyState()..field = 0; +} + +class MyState extends State { + int field = 0; +} +''', [ + lint(119, 20), + ]); + } + + test_blockBodyWithSingleStatement_returnsInstanceField() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + MyState instance = MyState(); + @override + MyState createState() { + return instance; + } +} + +class MyState extends State { + int field = 0; +} +''', [ + lint(161, 8), + ]); + } + + test_blockBodyWithSingleStatement_returnsState() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + @override + MyState createState() { + return MyState(); + } +} + +class MyState extends State { + int field = 0; +} +'''); + } + + test_blockBodyWithSingleStatement_returnsState_withCascade() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatefulWidget { + @override + MyState createState() { + return MyState()..field = 0; + } +} + +class MyState extends State { + int field = 0; +} +''', [ + lint(129, 20), + ]); + } + + test_instantiateTopLevel_returnTopLevel() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyStatefulBad extends StatefulWidget { + @override + MyState createState() { + global = MyState(); + return global; + } +} + +class MyState extends State { + int field = 0; +} + +var global = MyState(); +''', [ + lint(121, 48), + ]); + } +} diff --git a/pkg/linter/test/rules/no_self_assignments_test.dart b/pkg/linter/test/rules/no_self_assignments_test.dart new file mode 100644 index 00000000000..255b734aa1c --- /dev/null +++ b/pkg/linter/test/rules/no_self_assignments_test.dart @@ -0,0 +1,145 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoSelfAssignmentsTest); + }); +} + +@reflectiveTest +class NoSelfAssignmentsTest extends LintRuleTest { + @override + String get lintRule => 'no_self_assignments'; + + test_fieldAssignment() async { + await assertDiagnostics(r''' +class C { + int x = 5; + + C(int x) { + x = x; + } +} +''', [lint(41, 5)]); + } + + test_fieldAssignmentDifferentVar() async { + await assertNoDiagnostics(r''' +class C { + int x = 5; + + C(int y) { + x = y; + } +} +'''); + } + + test_fieldAssignmentExplicit() async { + await assertNoDiagnostics(r''' +class C { + int x = 5; + + C(int x) { + this.x = x; + } +} +'''); + } + + test_fieldAssignmentExplicitSameVar() async { + await assertDiagnostics(r''' +class C { + int x = 5; + + void update(C other) { + other.x = other.x; + } +} +''', [lint(53, 17)]); + } + + test_fieldAssignmentThisAndDifferentTarget() async { + await assertNoDiagnostics(r''' +class C { + int x = 5; + + void update(C other) { + this.x = other.x; + } +} +'''); + } + + test_fieldAssignmentDifferentTargets() async { + await assertNoDiagnostics(r''' +class C { + String hello = 'ok'; +} + +void test(C one, C two) { + one.hello = two.hello; +} +'''); + } + + test_fieldInitialization() async { + await assertNoDiagnostics(r''' +class C { + int x; + + C(int x) : x = x; +} +'''); + } + + test_propertyAssignment() async { + await assertDiagnostics(r''' +class C { + int _x = 5; + + int get x => _x; + + set x(int x) { + _x = x; + } + + void example() { + x = x; + } +} +''', [lint(102, 5)]); + } + + test_classMemberAssignment() async { + await assertDiagnostics(r''' +class C { + static String foo = "foo"; +} + +void main() { + C.foo = C.foo; +} +''', [lint(58, 13)]); + } + + test_classMemberAssignmentUnrelated() async { + await assertNoDiagnostics(r''' +class C { + static String foo = "foo"; +} + +void main() { + String foo; + foo = C.foo; // OK + print(foo); +} +'''); + } +} diff --git a/pkg/linter/test/rules/no_wildcard_variable_uses_test.dart b/pkg/linter/test/rules/no_wildcard_variable_uses_test.dart new file mode 100644 index 00000000000..808664d281b --- /dev/null +++ b/pkg/linter/test/rules/no_wildcard_variable_uses_test.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoWildcardVariableUsesTest); + }); +} + +@reflectiveTest +class NoWildcardVariableUsesTest extends LintRuleTest { + @override + String get lintRule => 'no_wildcard_variable_uses'; + + test_constructor() async { + await assertNoDiagnostics(r''' +class C { + C._(); + m() { + print(C._); + print(C._()); + } +} +'''); + } + + test_declaredIdentifier() async { + await assertNoDiagnostics(r''' +f() { + for (var _ in [1, 2, 3]) ; +} +'''); + } + + test_field() async { + await assertNoDiagnostics(r''' +class C { + int _ = 0; + m() { + print(_); + } +} +'''); + } + + test_getter() async { + await assertNoDiagnostics(r''' +class C { + int get _ => 0; + m() { + print(_); + } +} +'''); + } + + test_localVar() async { + await assertDiagnostics(r''' +f() { + var _ = 1; + print(_); +} +''', [ + lint(27, 1), + ]); + } + + test_method() async { + await assertNoDiagnostics(r''' +class C { + String _() => ''; + m() { + print(_); + print(_()); + } +} +'''); + } + + test_param() async { + await assertDiagnostics(r''' +f(int __) { + print(__); +} +''', [ + lint(20, 2), + ]); + } + + test_topLevelFunction() async { + await assertNoDiagnostics(r''' +String _() => ''; + +f() { + print(_); + print(_()); +} +'''); + } + + test_topLevelGetter() async { + await assertNoDiagnostics(r''' +int get _ => 0; + +f() { + print(_); +} +'''); + } +} diff --git a/pkg/linter/test/rules/non_constant_identifier_names_test.dart b/pkg/linter/test/rules/non_constant_identifier_names_test.dart new file mode 100644 index 00000000000..bf40eaebccd --- /dev/null +++ b/pkg/linter/test/rules/non_constant_identifier_names_test.dart @@ -0,0 +1,240 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NonConstantIdentifierNamesTest); + defineReflectiveTests(NonConstantIdentifierNamesRecordsTest); + defineReflectiveTests(NonConstantIdentifierNamesPatternsTest); + }); +} + +@reflectiveTest +class NonConstantIdentifierNamesPatternsTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'non_constant_identifier_names'; + + test_extensionType_representationConstructorName() async { + await assertDiagnostics(r''' +extension type e.Efg(int i) {} +''', [ + lint(17, 3), + ]); + } + + test_patternForStatement() async { + await assertDiagnostics(r''' +void f() { + for (var (AB, c) = (0, 1); AB <= 13; (AB, c) = (c, AB + c)) { } +} +''', [ + lint(23, 2), + ]); + } + + test_patternIfStatement() async { + await assertDiagnostics(r''' +void f() { + if ([1,2] case [int AB, int c]) { } +} +''', [ + lint(33, 2), + ]); + } + + test_patternIfStatement_recordField() async { + await assertDiagnostics(r''' +void f(Object o) { + if (o case (a: int AB, BC: int CD)) { } +} +''', [ + lint(40, 2), + lint(52, 2), + ]); + } + + test_patternIfStatement_recordField_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + if (o case (:int AB, var b)) { } +} +'''); + } + + test_patternIfStatement_underscores() async { + await assertNoDiagnostics(r''' +void f() { + if ([1,2] case [int _, int _]) { } +} +'''); + } + + test_patternList_declaration() async { + await assertDiagnostics(r''' +f() { + var [__, foo_bar] = [1,2]; +} +''', [ + lint(13, 2), + lint(17, 7), + ]); + } + + test_patternList_declaration_underscore_ok() async { + await assertNoDiagnostics(r''' +f() { + var [_, a] = [1,2]; +} +'''); + } + + test_patternList_wildCardPattern() async { + await assertNoDiagnostics(r''' +f() { + var a = 0; + [_, a] = [1,2]; +} +'''); + } + + test_patternRecordField() async { + await assertDiagnostics(r''' +void f() { + var (AB, ) = (1, ); +} +''', [ + lint(18, 2), + ]); + } + + test_patternRecordField_shortcut_ok() async { + await assertNoDiagnostics(r''' +f(Object o) { + switch(o) { + case (:int AB, var b): + } + switch(o) { + case (:int AB?, var b): + case (:int AB!, var b): + } +} +'''); + } + + test_patternRecordField_underscores() async { + await assertDiagnostics(r''' +void f() { + var (___, ) = (1, ); +} +''', [ + lint(18, 3), + ]); + } +} + +@reflectiveTest +class NonConstantIdentifierNamesRecordsTest extends LintRuleTest { + @override + String get lintRule => 'non_constant_identifier_names'; + + test_recordFields() async { + await assertDiagnostics(r''' +var a = (x: 1); +var b = (XX: 1); +''', [ + lint(25, 2), + ]); + } + + test_recordFields_fieldNameDuplicated() async { + // This will produce a compile-time error and we don't want to over-report. + await assertDiagnostics(r''' +var r = (a: 1, a: 2); +''', [ + // No Lint. + error(CompileTimeErrorCode.DUPLICATE_FIELD_NAME, 15, 1), + ]); + } + + test_recordFields_fieldNameFromObject() async { + // This will produce a compile-time error and we don't want to over-report. + await assertDiagnostics(r''' +var a = (hashCode: 1); +''', [ + // No Lint. + error(CompileTimeErrorCode.INVALID_FIELD_NAME_FROM_OBJECT, 9, 8), + ]); + } + + test_recordFields_fieldNamePositional() async { + // This will produce a compile-time error and we don't want to over-report. + await assertDiagnostics(r''' +var r = (0, $1: 2); +''', [ + // No Lint. + error(CompileTimeErrorCode.INVALID_FIELD_NAME_POSITIONAL, 12, 2), + ]); + } + + test_recordFields_privateFieldName() async { + // This will produce a compile-time error and we don't want to over-report. + await assertDiagnostics(r''' +var a = (_x: 1); +''', [ + // No Lint. + error(CompileTimeErrorCode.INVALID_FIELD_NAME_PRIVATE, 9, 2), + ]); + } + + test_recordTypeAnnotation_named() async { + await assertDiagnostics(r''' +(int, {String SS, bool b})? triple; +''', [ + lint(14, 2), + ]); + } + + test_recordTypeAnnotation_positional() async { + await assertDiagnostics(r''' +(int, String SS, bool) triple = (1,'', false); +''', [ + lint(13, 2), + ]); + } + + test_recordTypeDeclarations() async { + await assertDiagnostics(r''' +var AA = (x: 1); +const BB = (x: 1); +''', [ + lint(4, 2), + ]); + } +} + +@reflectiveTest +class NonConstantIdentifierNamesTest extends LintRuleTest { + @override + String get lintRule => 'non_constant_identifier_names'; + + ///https://github.com/dart-lang/linter/issues/193 + test_ignoreSyntheticNodes() async { + await assertDiagnostics(r''' +class C { } +C; +''', [ + // No lint + error(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 15, 1), + error(CompileTimeErrorCode.DUPLICATE_DEFINITION, 15, 1), + error(ParserErrorCode.MISSING_FUNCTION_BODY, 21, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/noop_primitive_operations_test.dart b/pkg/linter/test/rules/noop_primitive_operations_test.dart new file mode 100644 index 00000000000..ea06eb6d7f0 --- /dev/null +++ b/pkg/linter/test/rules/noop_primitive_operations_test.dart @@ -0,0 +1,175 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoopPrimitiveOperationsTest); + }); +} + +@reflectiveTest +class NoopPrimitiveOperationsTest extends LintRuleTest { + @override + String get lintRule => 'noop_primitive_operations'; + + test_double_toDouble() async { + await assertDiagnostics(r''' +void f(double x) { + x.toDouble(); +} +''', [ + lint(23, 8), + ]); + } + + test_int_ceil() async { + await assertDiagnostics(r''' +void f(int x) { + x.ceil(); +} +''', [ + lint(20, 4), + ]); + } + + test_int_floor() async { + await assertDiagnostics(r''' +void f(int x) { + x.floor(); +} +''', [ + lint(20, 5), + ]); + } + + test_int_round() async { + await assertDiagnostics(r''' +void f(int x) { + x.round(); +} +''', [ + lint(20, 5), + ]); + } + + test_int_toInt() async { + await assertDiagnostics(r''' +void f(int x) { + x.toInt(); +} +''', [ + lint(20, 5), + ]); + } + + test_int_truncate() async { + await assertDiagnostics(r''' +void f(int x) { + x.truncate(); +} +''', [ + lint(20, 8), + ]); + } + + test_interpolation_object_toString() async { + await assertDiagnostics(r''' +void f() { + '${1.toString()}'; +} +''', [ + lint(18, 8), + ]); + } + + test_interpolation_super_toString() async { + await assertNoDiagnostics(r''' +class C { + void m() { + '${super.toString()}'; + } +} +'''); + } + + test_print_null_toString() async { + await assertDiagnostics(r''' +void f() { + print(null.toString()); +} +''', [ + lint(24, 8), + ]); + } + + test_print_object_toString() async { + await assertDiagnostics(r''' +void f() { + print(1.toString()); +} +''', [ + lint(21, 8), + ]); + } + + test_print_stringLiteral() async { + await assertNoDiagnostics(r''' +onPrint() { + print(''); // OK +} +'''); + } + + test_string_adjacentBlankString() async { + await assertDiagnostics(r''' +void f(String x) { + x = 'hello\n' 'world\n' ''; +} +''', [ + lint(45, 2), + ]); + } + + test_string_nullable_toString() async { + await assertNoDiagnostics(r''' +void f(String? x) { + x.toString(); +} +'''); + } + + test_string_toString() async { + await assertDiagnostics(r''' +void f(String x) { + x.toString(); +} +''', [ + lint(23, 8), + ]); + } + + test_super_toString() async { + await assertNoDiagnostics(r''' +class C { + void m() { + super.toString(); + } +} +'''); + } + + test_unrelatedToStringFunction() async { + await assertNoDiagnostics(r''' +void f() { + print(toString()); +} + +String toString() => ''; +'''); + } +} diff --git a/pkg/linter/test/rules/null_check_on_nullable_type_parameter_test.dart b/pkg/linter/test/rules/null_check_on_nullable_type_parameter_test.dart new file mode 100644 index 00000000000..44ec21500cc --- /dev/null +++ b/pkg/linter/test/rules/null_check_on_nullable_type_parameter_test.dart @@ -0,0 +1,258 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NullCheckOnNullableTypeParameterTest); + }); +} + +@reflectiveTest +class NullCheckOnNullableTypeParameterTest extends LintRuleTest { + @override + String get lintRule => 'null_check_on_nullable_type_parameter'; + + test_expectedDynamic() async { + await assertNoDiagnostics(r''' +dynamic f(T? p) => p!; +'''); + } + + test_expectedSameTypeVariable_arrow() async { + await assertDiagnostics(r''' +T f(T? p) => p!; +''', [ + lint(17, 1), + ]); + } + + test_expectedSameTypeVariable_assignment() async { + await assertDiagnostics(r''' +void f(T? p) { + T t; + t = p!; +} +''', [ + lint(32, 1), + ]); + } + + test_expectedSameTypeVariable_assignmentInDeclaration() async { + await assertDiagnostics(r''' +void f(T? p) { T t = p!; } +''', [ + lint(25, 1), + ]); + } + + test_expectedSameTypeVariable_block() async { + await assertDiagnostics(r''' +T f(T? p) { return p!; } +''', [ + lint(23, 1), + ]); + } + + test_inAsExpression() async { + await assertNoDiagnostics(r''' +R f(P? p) => p! as R; +'''); + } + + test_inAssignmentToNonNullableField() async { + await assertDiagnostics(r''' +class C { + late T t; + void f(T? p) { + t = p!; + } +} +''', [ + lint(51, 1), + ]); + } + + test_inListLiteral() async { + await assertDiagnostics(r''' +List f(T? p) => [p!]; +''', [ + lint(24, 1), + ]); + } + + test_inMapLiteralKey() async { + await assertDiagnostics(r''' +Map f(T? p) => {p!: ''}; +''', [ + lint(31, 1), + ]); + } + + test_inMapLiteralValue() async { + await assertDiagnostics(r''' +Map f(T? p) => {'': p!}; +''', [ + lint(35, 1), + ]); + } + + test_inSetLiteral() async { + await assertDiagnostics(r''' +Set f(T? p) => {p!}; +''', [ + lint(23, 1), + ]); + } + + test_inTargetOfObjectGetterCall() async { + await assertNoDiagnostics(r''' +int f(T? p) => p!.hashCode; +'''); + } + + test_inTargetOfObjectMethodCall() async { + await assertNoDiagnostics(r''' +String f(T? p) => p!.toString(); +'''); + } + + test_inTargetOfObjectMethodCall_cascade() async { + await assertNoDiagnostics(r''' +T f(T? p) => p!..toString(); +'''); + } + + test_inYieldAsync() async { + await assertDiagnostics(r''' +Stream f(T? p) async* {yield p!;} +''', [ + lint(36, 1), + ]); + } + + test_inYieldSync() async { + await assertDiagnostics(r''' +Iterable f(T? p) sync* {yield p!;} +''', [ + lint(37, 1), + ]); + } + + test_nullAssertPattern_ifCase() async { + await assertDiagnostics(r''' +f(T? x){ + if (x case var y!) print(y); +} +''', [ + lint(30, 1), + ]); + } + + test_nullAssertPattern_list() async { + await assertDiagnostics(r''' +f(List l){ + var [x!, y] = l; +} +''', [ + lint(26, 1), + ]); + } + + test_nullAssertPattern_logicalOr() async { + await assertDiagnostics(r''' +f(T? x){ + switch(x) { + case var y! || var y! : print(y); + } +} +''', [ + lint(40, 1), + error(WarningCode.DEAD_CODE, 42, 9), + lint(50, 1), + ]); + } + + test_nullAssertPattern_map() async { + await assertDiagnostics(r''' +f(Map m){ + var {'x': y!} = m; +} +''', [ + lint(38, 1), + ]); + } + + test_nullAssertPattern_object() async { + await assertDiagnostics(r''' +class A { + E? a; + A(this.a); +} + +f(T? t, A u) { + A(a: t!) = u; +} +''', [ + lint(66, 1), + ]); + } + + test_nullAssertPattern_record() async { + await assertDiagnostics(r''' +f((T?, T?) p){ + var (x!, y) = p; +} +''', [ + lint(26, 1), + ]); + } + + test_potentiallyNonNullableTypeVariable() async { + await assertNoDiagnostics(r''' +T f(T p) => p!; // OK +'''); + } + + test_typeParameterBoundToDynamic() async { + await assertDiagnostics(r''' +T f(T? p) => p!; +''', [ + lint(33, 1), + ]); + } + + test_typeParameterBoundToNullableObject() async { + await assertDiagnostics(r''' +T f(T? p) => p!; +''', [ + lint(33, 1), + ]); + } + + test_typeParameterBoundToObject() async { + await assertNoDiagnostics(r''' +T f(T? p) => p!; +'''); + } + + test_typeParameterExtendsNullableObject_inAwaited() async { + await assertDiagnostics(r''' +Future f(T? p) async => await p!; +''', [ + lint(53, 1), + ]); + } + + test_typeParameterExtendsNullableObject_inAwaitedInList() async { + await assertDiagnostics(r''' +Future> f(T? p) async => await [p!]; +''', [ + lint(60, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/null_closures_test.dart b/pkg/linter/test/rules/null_closures_test.dart new file mode 100644 index 00000000000..8761818c85a --- /dev/null +++ b/pkg/linter/test/rules/null_closures_test.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(NullClosuresPreNNBDTest); + defineReflectiveTests(NullClosuresTest); + }); +} + +@reflectiveTest +class NullClosuresPreNNBDTest extends LintRuleTest { + @override + String get lintRule => 'null_closures'; + + @override + setUp() { + super.setUp(); + noSoundNullSafety = false; + } + + tearDown() { + noSoundNullSafety = true; + } + + test_list_firstWhere() async { + await assertNoDiagnostics(r''' +// @dart=2.9 + +f() => [2, 4, 6].firstWhere((e) => e.isEven, orElse: () => null); +'''); + } + + test_list_generate() async { + await assertDiagnostics(r''' +// @dart=2.9 + +f() => List.generate(3, null); +''', [ + lint(38, 4), + ]); + } + + test_list_where() async { + await assertDiagnostics(r''' +// @dart=2.9 + +f() => [2, 4, 6].where(null); +''', [ + lint(42, 4), + ]); + } + + test_map_putIfAbsent() async { + await assertDiagnostics(r''' +// @dart=2.9 + +f() { + var map = {}; + map.putIfAbsent(7, null); + return map; +} +''', [ + lint(67, 4), + ]); + } +} + +@reflectiveTest +class NullClosuresTest extends LintRuleTest { + @override + String get lintRule => 'null_closures'; + + ///https://github.com/dart-lang/linter/issues/1414 + test_recursiveInterfaceInheritance() async { + await assertDiagnostics(r''' +class A extends B { + A(int x); +} + +class B extends A {} + +void test_cycle() { + A(null); +} +''', [ + // No lint + error(CompileTimeErrorCode.RECURSIVE_INTERFACE_INHERITANCE, 6, 1), + error(CompileTimeErrorCode.RECURSIVE_INTERFACE_INHERITANCE, 41, 1), + error(CompileTimeErrorCode.NO_DEFAULT_SUPER_CONSTRUCTOR_IMPLICIT, 41, 1), + error(CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE, 81, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/omit_local_variable_types_test.dart b/pkg/linter/test/rules/omit_local_variable_types_test.dart new file mode 100644 index 00000000000..940b3470e45 --- /dev/null +++ b/pkg/linter/test/rules/omit_local_variable_types_test.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(OmitLocalVariableTypesTest); + }); +} + +@reflectiveTest +class OmitLocalVariableTypesTest extends LintRuleTest { + @override + String get lintRule => 'omit_local_variable_types'; + + /// Types are considered an important part of the pattern so we + /// intentionally do not lint on declared variable patterns. + test_listPattern_destructured() async { + await assertNoDiagnostics(r''' +f() { + var [int a] = [1]; +} +'''); + } + + test_mapPattern_destructured() async { + await assertNoDiagnostics(r''' +f() { + var {'a': int a} = {'a': 1}; +} +'''); + } + + test_objectPattern_destructured() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + final A(a: int _b) = A(1); +} +'''); + } + + test_objectPattern_switch() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && int b): + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3016 + @failingTest + test_paramIsType() async { + await assertDiagnostics(r''' +T bar(T d) => d; + +String f() { + String h = bar(''); + return h; +} +''', [ + lint(42, 26), + ]); + } + + test_record_destructured() async { + await assertNoDiagnostics(r''' +f(Object o) { + switch (o) { + case (int x, String s): + } +} +'''); + } + + test_recordPattern_switch() async { + await assertNoDiagnostics(r''' +f() { + switch ((1, 2)) { + case (int a, final int b): + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3016 + test_typeNeededForInference() async { + await assertNoDiagnostics(r''' +T bar(dynamic d) => d; + +String f() { + String h = bar(''); + return h; +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3016 + test_typeParamProvided() async { + await assertDiagnostics(r''' +T bar(dynamic d) => d; + +String f() { + String h = bar(''); + return h; +} +''', [ + lint(42, 26), + ]); + } +} diff --git a/pkg/linter/test/rules/one_member_abstracts_test.dart b/pkg/linter/test/rules/one_member_abstracts_test.dart new file mode 100644 index 00000000000..d5ac771ba9c --- /dev/null +++ b/pkg/linter/test/rules/one_member_abstracts_test.dart @@ -0,0 +1,119 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(OneMemberAbstractsTest); + }); +} + +@reflectiveTest +class OneMemberAbstractsTest extends LintRuleTest { + @override + String get lintRule => 'one_member_abstracts'; + + test_oneMember_abstract() async { + await assertDiagnostics(r''' +abstract class C { + void m(); +} +''', [ + lint(15, 1), + ]); + } + + test_oneMember_abstractGetter() async { + await assertNoDiagnostics(r''' +abstract class C { + String get s; +} +'''); + } + + test_oneMember_appliedMixin() async { + await assertNoDiagnostics(r''' +abstract class C with M { + void m2(); +} + +mixin M { + void m1(); +} +'''); + } + + test_oneMember_extendedType() async { + await assertNoDiagnostics(r''' +abstract class D extends C { + int m2(); +} + +abstract class C { + int m2() => 42; +} +'''); + } + + test_oneMember_implementedInterface() async { + await assertNoDiagnostics(r''' +abstract class D implements C { + void m3(); +} + +abstract class C { + void m1(); + void m2(); +} +'''); + } + + test_oneMember_nonAbstract() async { + await assertNoDiagnostics(r''' +abstract class C { + int f() => 42; +} +'''); + } + + test_sealed_concreteMethod_noDiagnostic() async { + await assertNoDiagnostics(r''' +sealed class C { + void f() { } +} +'''); + } + + test_sealed_noDiagnostic() async { + await assertNoDiagnostics(r''' +sealed class C { + void f(); +} +'''); + } + + test_twoMembers() async { + await assertNoDiagnostics(r''' +abstract class C { + int x = 0; + int f(); +} +'''); + } + + test_zeroMember_extendedTypeHasOneMember() async { + await assertDiagnostics(r''' +abstract class D extends C {} + +abstract class C { + void m(); +} +''', [ + lint(46, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/only_throw_errors_test.dart b/pkg/linter/test/rules/only_throw_errors_test.dart new file mode 100644 index 00000000000..575c6d03b95 --- /dev/null +++ b/pkg/linter/test/rules/only_throw_errors_test.dart @@ -0,0 +1,134 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(OnlyThrowErrorsTest); + }); +} + +@reflectiveTest +class OnlyThrowErrorsTest extends LintRuleTest { + @override + String get lintRule => 'only_throw_errors'; + + @override + setUp() { + super.setUp(); + noSoundNullSafety = false; + } + + tearDown() { + noSoundNullSafety = true; + } + + test_argumentError() async { + await assertNoDiagnostics(r''' +void f() { + throw ArgumentError('hello'); +} +'''); + } + + test_error() async { + await assertNoDiagnostics(r''' +void f() { + throw Error(); +} +'''); + } + + test_exception() async { + await assertNoDiagnostics(r''' +void f() { + throw ArgumentError('hello'); +} +'''); + } + + test_exceptionGeneric() async { + await assertNoDiagnostics(r''' +void f(E error) { + throw error; +} +'''); + } + + test_exceptionGenericUnboundedAndPromoted() async { + await assertNoDiagnostics(r''' +void f(E error) { + if (error is Error) { + throw error; + } +} +'''); + } + + test_exceptionMixedIn() async { + await assertNoDiagnostics(r''' +// @dart=2.19 +class Err extends Object with Exception {} + +void f() { + throw Err(); +} +'''); + } + + test_int() async { + await assertDiagnostics(r''' +void f() { + throw 7; +} +''', [ + lint(19, 1), + ]); + } + + test_nullInPreNullSafe() async { + await assertDiagnostics(r''' +// @dart=2.9 +void f() { + throw null; +} +''', [ + lint(32, 4), + ]); + } + + test_object() async { + await assertDiagnostics(r''' +void f() { + throw Object(); +} +''', [ + lint(19, 8), + ]); + } + + test_string() async { + await assertDiagnostics(r''' +void f() { + throw 'hello'; +} +''', [ + lint(19, 7), + ]); + } + + test_never() async { + await assertNoDiagnostics(r''' +void f() { + throw e(); +} + +Never e() => throw Exception(); +'''); + } +} diff --git a/pkg/linter/test/rules/overridden_fields_test.dart b/pkg/linter/test/rules/overridden_fields_test.dart new file mode 100644 index 00000000000..e572a204a5b --- /dev/null +++ b/pkg/linter/test/rules/overridden_fields_test.dart @@ -0,0 +1,166 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(OverriddenFieldsTest); + }); +} + +@reflectiveTest +class OverriddenFieldsTest extends LintRuleTest { + @override + String get lintRule => 'overridden_fields'; + + /// https://github.com/dart-lang/linter/issues/2874 + test_conflictingStaticAndInstance() async { + await assertNoDiagnostics(r''' +class A { + static final String field = 'value'; +} + +class B extends A { + String field = 'otherValue'; +} +'''); + } + + test_externalLibrary() async { + newFile('$testPackageLibPath/a.dart', r''' +class A { + int? public; +} +'''); + await assertDiagnostics(r''' +import 'a.dart'; +class B extends A { + int? public; +} +''', [ + lint(44, 6), + ]); + } + + test_externalLibraryWithPrivateField() async { + newFile('$testPackageLibPath/a.dart', r''' +class A { + int? _private; +} +'''); + await assertDiagnostics(r''' +import 'a.dart'; +class B extends A { + int? _private; +} +''', [ + error(WarningCode.UNUSED_FIELD, 44, 8), + ]); + } + + test_mixinInheritsFromNotObject() async { + // See: https://github.com/dart-lang/linter/issues/2969 + // Preserves existing testing logic but has so many misuses of mixins that + // that it's hard to know how much tested logic is intentional. + await assertDiagnostics(r''' +class Base { + Object field = 'lorem'; + + Object something = 'change'; +} + +class Bad1 extends Base { + @override + final x = 1, field = 'ipsum'; +} + +class GC11 extends Bad1 { + @override + Object something = 'done'; + + Object gc33 = 'gc33'; +} + +class GC13 extends Object with Bad1 { + @override + Object something = 'done'; + + @override + Object field = 'lint'; +} + +abstract class GC21 extends GC11 { + @override + Object something = 'done'; +} + +class GC23 extends Object with GC13 { + @override + Object something = 'done'; + + @override + Object field = 'lint'; +} + +abstract class GC31 extends GC13 { + @override + Object something = 'done'; +} + +abstract class GC32 implements GC13 { + @override + Object something = 'done'; +} + +class GC33 extends GC21 with GC13 { + @override + Object something = 'done'; + + @override + Object gc33 = 'yada'; +} + +class GC34 extends GC33 { + @override + var x = 3; + + @override + Object gc33 = 'yada'; +} +''', [ + error(WarningCode.OVERRIDE_ON_NON_OVERRIDING_FIELD, 120, 1), + lint(127, 5), + lint(194, 9), + error(CompileTimeErrorCode.MIXIN_INHERITS_FROM_NOT_OBJECT, 273, 4), + lint(343, 5), + lint(418, 9), + error(CompileTimeErrorCode.MIXIN_INHERITS_FROM_NOT_OBJECT, 472, 4), + lint(500, 9), + lint(542, 5), + lint(617, 9), + error(CompileTimeErrorCode.MIXIN_INHERITS_FROM_NOT_OBJECT, 751, 4), + lint(779, 9), + lint(821, 4), + lint(883, 1), + lint(912, 4), + ]); + } + + test_recursiveInterfaceInheritance() async { + // Produces a recursive_interface_inheritance diagnostic. + await assertDiagnostics(r''' +class A extends B {} +class B extends A { + int field = 0; +} +''', [ + // No lint + error(CompileTimeErrorCode.RECURSIVE_INTERFACE_INHERITANCE, 6, 1), + error(CompileTimeErrorCode.RECURSIVE_INTERFACE_INHERITANCE, 27, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/parameter_assignments_test.dart b/pkg/linter/test/rules/parameter_assignments_test.dart new file mode 100644 index 00000000000..c6a7a15e7ff --- /dev/null +++ b/pkg/linter/test/rules/parameter_assignments_test.dart @@ -0,0 +1,390 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ParameterAssignmentsTest); + }); +} + +@reflectiveTest +class ParameterAssignmentsTest extends LintRuleTest { + @override + String get lintRule => 'parameter_assignments'; + + test_assignment_nullableParameter() async { + await assertDiagnostics(r''' +void f([int? p]) { + p ??= 8; + p = 42; +} +''', [ + lint(32, 6), + ]); + } + + test_assignment_nullableParameter_named() async { + await assertDiagnostics(r''' +void f({int? p}) { + p ??= 8; + p = 42; +} +''', [ + lint(32, 6), + ]); + } + + @FailingTest(reason: 'Closures not implemented') + test_closure_assignment() async { + await assertDiagnostics(r''' +void f() { + (int p) { + p = 2; + }(2); +} +''', [ + lint(27, 5), + ]); + } + + test_compoundAssignment() async { + await assertDiagnostics(r''' +void f(int p) { + p += 3; +} +''', [ + lint(18, 6), + ]); + } + + test_function_assignment() async { + await assertDiagnostics(r''' +void f(int p) { + p = 4; +} +''', [ + lint(18, 5), + ]); + } + + test_function_incrementAssignment() async { + await assertDiagnostics(r''' +void f(int p) { + p += 4; +} +''', [ + lint(18, 6), + ]); + } + + test_function_named_default() async { + await assertDiagnostics(r''' +void f({int p = 5}) { + print(p++); +} +''', [ + lint(30, 3), + ]); + } + + test_function_named_optional_ok() async { + await assertNoDiagnostics(r''' +void f({int? optional}) { + optional ??= 8; +} +'''); + } + + test_function_ok_noAssignment() async { + await assertNoDiagnostics(r''' +void f(String p) { + print(p); +} +'''); + } + + test_function_ok_shadow() async { + await assertNoDiagnostics(r''' +void f(String? p) { + if (p == null) { + int p = 2; + p = 3; + } +} +'''); + } + + test_function_positional_optional_assignedTwice() async { + await assertDiagnostics(r''' +void f([int? optional]) { + optional ??= 8; + optional ??= 16; +} +''', [ + error(StaticWarningCode.DEAD_NULL_AWARE_EXPRESSION, 59, 2), + lint(46, 15), + ]); + } + + test_function_positional_optional_ok() async { + await assertNoDiagnostics(r''' +void f([int? optional]) { + optional ??= 8; +} +'''); + } + + test_function_positional_optional_re_incremented() async { + await assertDiagnostics(r''' +void f([int? optional]) { + optional ??= 8; + optional += 16; +} +''', [ + lint(46, 14), + ]); + } + + test_function_positional_optional_reassigned() async { + await assertDiagnostics(r''' +void f([int? optional]) { + optional ??= 8; + optional = 16; +} +''', [ + lint(46, 13), + ]); + } + + test_function_postfix() async { + await assertDiagnostics(r''' +void f(int p) { + p++; +} +''', [ + lint(18, 3), + ]); + } + + test_function_prefix() async { + await assertDiagnostics(r''' +void f(int p) { + ++p; +} +''', [ + lint(18, 3), + ]); + } + + test_instanceMethod_assignment() async { + await assertDiagnostics(r''' +class A { + void m(int p) { + p = 4; + } +} +''', [ + lint(32, 5), + ]); + } + + test_instanceMethod_nonAssignment() async { + await assertNoDiagnostics(r''' +class A { + void m(String p) { + print(p); + } +} +'''); + } + + test_instanceSetter_assignment() async { + await assertDiagnostics(r''' +class A { + set x(int value) { + value = 5; + } +} +''', [ + lint(35, 9), + ]); + } + + // If and switch cases don't need verification since params aren't valid + // constant pattern expressions. + + test_listAssignment() async { + await assertDiagnostics(r''' +f(var b) { + [b] = [1]; +} +''', [ + lint(13, 3), + ]); + } + + test_localFunction() async { + await assertDiagnostics(r''' +void f(int p) { + void g() { + p = 3; + } + g(); +} +''', [ + lint(33, 5), + ]); + } + + test_mapAssignment() async { + await assertDiagnostics(r''' +f(var a) { + {'a': a} = {'a': 1}; +} +''', [ + lint(13, 8), + ]); + } + + test_member_setter() async { + await assertDiagnostics(r''' +class A { + set x(int v) { + v = 5; + } +} +''', [ + lint(31, 5), + ]); + } + + test_nullAwareAssignment_nonNullableParameter() async { + await assertDiagnostics(r''' +void f([int p = 42]) { + // ignore: dead_null_aware_expression + p ??= 8; +} +''', [ + lint(65, 7), + ]); + } + + test_nullAwareAssignment_nonNullableParameter_named() async { + await assertDiagnostics(r''' +void f({int p = 42}) { + // ignore: dead_null_aware_expression + p ??= 8; +} +''', [ + lint(65, 7), + ]); + } + + test_nullAwareAssignment_nullableParameter() async { + await assertNoDiagnostics(r''' +void f([int? p]) { + p ??= 8; +} +'''); + } + + test_nullAwareAssignment_nullableParameter_named() async { + await assertNoDiagnostics(r''' +void f({int? p}) { + p ??= 8; +} +'''); + } + + test_nullAwareAssignment_nullableParameter_promotedToNonNullable() async { + await assertDiagnostics(r''' +void f([int? p]) { + p ??= 8; + // ignore: dead_null_aware_expression + p ??= 16; +} +''', [ + lint(72, 8), + ]); + } + + test_objectAssignment() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f(var b) { + A(a: b) = A(1); +} +''', [ + lint(48, 7), + ]); + } + + test_postfixOperation() async { + await assertDiagnostics(r''' +void f(int p) { + p++; +} +''', [ + lint(18, 3), + ]); + } + + test_postfixOperation_named() async { + await assertDiagnostics(r''' +void f({int p = 5}) { + p++; +} +''', [ + lint(24, 3), + ]); + } + + test_recordAssignment() async { + await assertDiagnostics(r''' +void f(var a) { + var b = 0; + (a, b) = (1, 2); +} +''', [ + lint(31, 6), + ]); + } + + test_topLevelFunction_assignment() async { + await assertDiagnostics(r''' +void f(int p) { + p = 4; +} +''', [ + lint(18, 5), + ]); + } + + test_topLevelFunction_nonAssignment() async { + await assertNoDiagnostics(r''' +void f(String p) { + print(p); +} +'''); + } + + test_topLevelMethod_nullableParameter_assignment() async { + await assertNoDiagnostics(r''' +void f(String? p) { + if (p == null) { + int p = 2; + p = 3; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_adjacent_string_concatenation_test.dart b/pkg/linter/test/rules/prefer_adjacent_string_concatenation_test.dart new file mode 100644 index 00000000000..ca6e08d43f9 --- /dev/null +++ b/pkg/linter/test/rules/prefer_adjacent_string_concatenation_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferAdjacentStringConcatenationTest); + }); +} + +@reflectiveTest +class PreferAdjacentStringConcatenationTest extends LintRuleTest { + @override + String get lintRule => 'prefer_adjacent_string_concatenation'; + + test_concatenation() async { + await assertNoDiagnostics(r''' +var s = 'hello' ' world'; +'''); + } + + test_plusOperator() async { + await assertDiagnostics(r''' +var s = 'hello' + ' world'; +''', [ + lint(16, 1), + ]); + } + + test_plusOperator_inListLiteral() async { + await assertDiagnostics(r''' +var list = ['this is' + ' not allowed']; +''', [ + lint(22, 1), + ]); + } + + test_plusOperator_nonStringLiteralLeft() async { + await assertNoDiagnostics(r''' +var p = ''; +var s = p + 'hello'; +'''); + } + + test_plusOperator_nonStringLiteralRight() async { + await assertNoDiagnostics(r''' +var p = ''; +var s = 'hello' + p; +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_asserts_in_initializer_lists_test.dart b/pkg/linter/test/rules/prefer_asserts_in_initializer_lists_test.dart new file mode 100644 index 00000000000..e771a84b529 --- /dev/null +++ b/pkg/linter/test/rules/prefer_asserts_in_initializer_lists_test.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferAssertsInInitializerListsTest); + defineReflectiveTests(PreferAssertsInInitializerListsSuperTest); + }); +} + +@reflectiveTest +class PreferAssertsInInitializerListsSuperTest extends LintRuleTest { + @override + String get lintRule => 'prefer_asserts_in_initializer_lists'; + + test_super() async { + await assertDiagnostics(r''' +class A { + final int a; + A(this.a); +} + +class B extends A { + B(super.a) { + assert(a != 0); + } +} +''', [ + lint(80, 6), + ]); + } +} + +@reflectiveTest +class PreferAssertsInInitializerListsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_asserts_in_initializer_lists'; + + test_nonBoolExpression() async { + await assertDiagnostics(r''' +class A { + bool? f; + A() { + assert(() + { + f = true; + return false; + }); + } +} +''', [ + // No lint + error(CompileTimeErrorCode.NON_BOOL_EXPRESSION, 40, 50), + ]); + } + + test_firstStatement() async { + await assertDiagnostics(r''' +class A { + A.named(a) { + assert(a != null); + } +} + +''', [ + lint(29, 6), + ]); + } + + test_afterFirstStatement() async { + await assertNoDiagnostics(r''' +class A { + A.named(a) { + print(''); + assert(a != null); + } +} + +'''); + } + + test_initializer() async { + await assertNoDiagnostics(r''' +class A { + A.named(a) : assert(a != null); +} + +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_collection_literals_test.dart b/pkg/linter/test/rules/prefer_collection_literals_test.dart new file mode 100644 index 00000000000..9287c6fcedf --- /dev/null +++ b/pkg/linter/test/rules/prefer_collection_literals_test.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferCollectionLiteralsTest); + }); +} + +@reflectiveTest +class PreferCollectionLiteralsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_collection_literals'; + + @failingTest + test_closure_returns_linkedHashSet() async { + await assertDiagnostics(r''' +import 'dart:collection'; + +void a(Set Function() f) {} + +void c() { + a(() => LinkedHashSet()); +} +''', [ + lint(82, 20), + ]); + } + + /// https://github.com/dart-lang/linter/issues/2985 + test_linkedHashSetParameter_named_type_required() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; + +class Foo {} + +void a({required LinkedHashSet some}) {} + +void c() { + a(some: LinkedHashSet()); +} +'''); + } + + test_linkedHashSetParameter_named_type_unrequired() async { + await assertDiagnostics(r''' +import 'dart:collection'; + +class Foo {} + +void a({required Set some}) {} + +void c() { + a(some: LinkedHashSet()); +} +''', [ + lint(103, 20), + ]); + } + + test_linkedHashSetParameter_type_required() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; + +class Foo {} + +void b(LinkedHashSet some) {} + +void c() { + b(LinkedHashSet()); +} +'''); + } + + test_linkedHashSetParameter_type_unrequired() async { + await assertDiagnostics(r''' +import 'dart:collection'; + +class Foo {} + +void b(Set some) {} + +void c() { + b(LinkedHashSet()); +} +''', [ + lint(86, 20), + ]); + } + + test_undefinedFunction() async { + await assertDiagnostics(r''' +import 'dart:collection'; + +void f() { + printUnresolved(LinkedHashSet()); +} +''', [ + // No lints. + error(CompileTimeErrorCode.UNDEFINED_FUNCTION, 44, 15), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_const_constructors_in_immutables_test.dart b/pkg/linter/test/rules/prefer_const_constructors_in_immutables_test.dart new file mode 100644 index 00000000000..039e77ddc45 --- /dev/null +++ b/pkg/linter/test/rules/prefer_const_constructors_in_immutables_test.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConstConstructorsInImmutablesTest); + }); +} + +@reflectiveTest +class PreferConstConstructorsInImmutablesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'prefer_const_constructors_in_immutables'; + + test_assertInitializer_canBeConst() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class C { + C.named(a) : assert(a != null); +} +''', [ + lint(57, 1), + ]); + } + + test_assertInitializer_cannotBeConst() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class C { + C.named(a) : assert(a.toString() == 'string'); +} +'''); + } + + test_implicitSuperConstructorInvocation_undefined() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class A { + const A.named(); +} + +class B extends A { + B(); +} +''', [ + error(CompileTimeErrorCode.UNDEFINED_CONSTRUCTOR_IN_INITIALIZER_DEFAULT, + 99, 1), + ]); + } + + test_returnOfInvalidType() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class F { + factory F.fc() => null; +} +''', [ + // No lint + error( + CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_CONSTRUCTOR, 75, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_const_constructors_test.dart b/pkg/linter/test/rules/prefer_const_constructors_test.dart new file mode 100644 index 00000000000..9a292f88ed1 --- /dev/null +++ b/pkg/linter/test/rules/prefer_const_constructors_test.dart @@ -0,0 +1,313 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConstConstructorsTest); + }); +} + +@reflectiveTest +class PreferConstConstructorsTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'prefer_const_constructors'; + + test_canBeConst_argumentIsAdjacentStrings() async { + await assertDiagnostics(r''' +class A { + const A(String s); +} +var a = A('adjacent' 'string'); +''', [ + lint(41, 22), + ]); + } + + test_canBeConst_argumentIsListLiteral() async { + await assertDiagnostics(r''' +class A { + const A(List l); +} +var a = A([]); +''', [ + lint(44, 5), + ]); + } + + test_canBeConst_argumentIsMap_nonLiteral() async { + await assertNoDiagnostics(r''' +class A { + const A(Map m); +} +A f(Map m) => A(m); +'''); + } + + test_canBeConst_argumentIsMapLiteral_containsNonLiteral() async { + await assertNoDiagnostics(r''' +class A { + const A(Map m); +} +A f(int x) => A({x: x}); +'''); + } + + test_canBeConst_argumentIsMapLiteral_instantiated() async { + await assertDiagnostics(r''' +class A { + const A(Map m); +} +var a = A({}); +''', [ + lint(48, 5), + ]); + } + + test_canBeConst_explicitTypeArgument_dynamic() async { + await assertDiagnostics(r''' +class A { + const A(); +} +var a = A(); +''', [ + lint(36, 12), + ]); + } + + test_canBeConst_explicitTypeArgument_string() async { + await assertDiagnostics(r''' +class A { + const A(); +} +var a = A(); +''', [ + lint(36, 11), + ]); + } + + test_canBeConst_intLiteralArgument() async { + await assertDiagnostics(r''' +class A { + const A(int x); +} +var a = A(5); +''', [ + lint(38, 4), + ]); + } + + test_canBeConst_optionalNamedParameter() async { + await assertDiagnostics(r''' +class A { + const A({A? parent}); +} +var a = A(); +''', [ + lint(44, 3), + ]); + } + + test_canBeConst_optionalNamedParameter_nested() async { + await assertDiagnostics(r''' +class A { + const A({A? parent}); + const A.a(); +} +var a = A( + parent: A.a(), +); +''', [ + lint(59, 21), + lint(72, 5), + ]); + } + + test_canBeConst_optionalNamedParameter_newKeyword() async { + await assertDiagnostics(r''' +class A { + const A({A? parent}); +} +var a = new A(); +''', [ + lint(44, 7), + ]); + } + + test_cannotBeConst_argumentIsAdjacentStrings_withInterpolation() async { + await assertNoDiagnostics(r''' +class A { + const A(String s); +} +A f(int i) => A('adjacent' '$i'); +'''); + } + + test_cannotBeConst_argumentIsListLiteral_nonLiteralElement() async { + await assertNoDiagnostics(r''' +class A { + const A(List l); +} +A f(int i) => A([i]); +'''); + } + + test_cannotBeConst_argumentIsLocalVariable() async { + await assertNoDiagnostics(r''' +class A { + const A(String s); +} + +void f() { + final s = ''; + var a = A(s); +} +'''); + } + + test_cannotBeConst_argumentIsNonLiteral() async { + await assertNoDiagnostics(r''' +class A { + const A(String s); +} +A f(String s) => A(s); +'''); + } + + test_cannotBeConst_argumentIsNonLiteralList() async { + await assertNoDiagnostics(r''' +class A { + const A(List l); +} +A f(List l) => A(l); +'''); + } + + test_cannotBeConst_explicitTypeArgument_typeVariable() async { + await assertNoDiagnostics(r''' +class A { + const A(); +} +void f() => A(); +'''); + } + + test_cannotBeConst_nonConstArgument() async { + await assertNoDiagnostics(r''' +class A { + final int x; + + const A(this.x); +} +A f(int x) => A(x); +'''); + } + + test_cannotBeConst_notConstConstructor() async { + await assertNoDiagnostics(r''' +class A { + A(); +} +var a = A(); +'''); + } + + test_cannotBeConst_stringLiteralArgument_withInterpolation() async { + await assertNoDiagnostics(r''' +class A { + const A(String s); + static A m1(int i) => A('$i'); +} +'''); + } + + test_deferred_arg() async { + newFile2('$testPackageLibPath/a.dart', ''' +class A { + const A(); +} + +const aa = A(); +'''); + + await assertNoDiagnostics(r''' +import 'a.dart' deferred as a; + +class B { + const B(Object a); +} + +main() { + var b = B(a.aa); +} +'''); + } + + test_deferredConstructorCall() async { + newFile2('$testPackageLibPath/a.dart', ''' +class A { + const A(); +} +'''); + + await assertNoDiagnostics(r''' +import 'a.dart' deferred as a; + +void f() { + var aa = a.A(); +} +'''); + } + + test_extraPositionalArgument() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +class K { + @literal + const K(); +} + +K k() { + var kk = K(); + return kk; +} +''', [ + // No lint + error(WarningCode.NON_CONST_CALL_TO_LITERAL_CONSTRUCTOR, 90, 3), + ]); + } + + test_isConst_intLiteralArgument() async { + await assertNoDiagnostics(r''' +class A { + final int x; + + const A(this.x); +} +A f() => const A(5); +'''); + } + + test_isConstCall_optionalNamedParameter() async { + await assertNoDiagnostics(r''' +class A { + const A({A? parent}); +} +var a = const A(); +'''); + } + + test_objectConstructorCall() async { + await assertNoDiagnostics(r''' +var x = Object(); +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_const_declarations_test.dart b/pkg/linter/test/rules/prefer_const_declarations_test.dart new file mode 100644 index 00000000000..8a6728f5a83 --- /dev/null +++ b/pkg/linter/test/rules/prefer_const_declarations_test.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConstDeclarationsTest); + }); +} + +@reflectiveTest +class PreferConstDeclarationsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_const_declarations'; + + test_recordLiteral() async { + await assertDiagnostics(r''' +final tuple = const ("first", 2, true); +''', [ + lint(0, 38), + ]); + } + + test_test_recordLiteral_nonConst() async { + await assertNoDiagnostics(r''' +final tuple = (1, () {}); +'''); + } + + test_test_recordLiteral_ok() async { + await assertNoDiagnostics(r''' +const record = (number: 123, name: "Main", type: "Street"); +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_const_literals_to_create_immutables_test.dart b/pkg/linter/test/rules/prefer_const_literals_to_create_immutables_test.dart new file mode 100644 index 00000000000..6d10ae968bf --- /dev/null +++ b/pkg/linter/test/rules/prefer_const_literals_to_create_immutables_test.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConstLiteralsToCreateImmutablesTest); + }); +} + +@reflectiveTest +class PreferConstLiteralsToCreateImmutablesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'prefer_const_literals_to_create_immutables'; + + test_missingRequiredArgument() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@immutable +class K { + final List children; + const K({required this.children}); +} + +final k = K( + children: [for (var i = 0; i < 5; ++i) K()], // OK +); +''', [ + // No lint + error(CompileTimeErrorCode.MISSING_REQUIRED_ARGUMENT, 178, 1), + ]); + } + + test_newWithNonType() async { + await assertDiagnostics(r''' +var e1 = new B([]); // OK +''', [ + // No lint + error(CompileTimeErrorCode.NEW_WITH_NON_TYPE, 13, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_constructors_over_static_methods_test.dart b/pkg/linter/test/rules/prefer_constructors_over_static_methods_test.dart new file mode 100644 index 00000000000..15a2770a514 --- /dev/null +++ b/pkg/linter/test/rules/prefer_constructors_over_static_methods_test.dart @@ -0,0 +1,141 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConstructorsOverStaticMethodsTest); + }); +} + +@reflectiveTest +class PreferConstructorsOverStaticMethodsTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'prefer_constructors_over_static_methods'; + + test_extensionMethod() async { + await assertNoDiagnostics(r''' +class A { + A.named(); +} +extension E on A { + static A foo() => A.named(); +} +'''); + } + + test_factoryConstructor() async { + await assertNoDiagnostics(r''' +class A { + A.named(); + factory A.f() { + return A.named(); + } +} +'''); + } + + test_staticGetter() async { + await assertDiagnostics(r''' +class A { + A.named(); + static A get getter => A.named(); +} +''', [ + lint(38, 6), + ]); + } + + test_staticMethod_expressionBody() async { + await assertDiagnostics(r''' +class A { + A.named(); + static A staticM() => A.named(); +} +''', [ + lint(34, 7), + ]); + } + + test_staticMethod_expressionBody_extensionType() async { + // Since the check logic is shared one test should be sufficient to verify + // extension types are supported. + await assertDiagnostics(r''' +extension type E(int i) { + static E make(int i) => E(i); +} +''', [ + lint(37, 4), + ]); + } + + test_staticMethod_generic() async { + await assertNoDiagnostics(r''' +class A { + A.named(); + static A generic() => A.named(); +} +'''); + } + + test_staticMethod_referenceToConstructedInstanceOfClass() async { + await assertDiagnostics(r''' +class A { + A.named(); + static A instanceM() { + final a = A.named(); + return a; + } +} +''', [ + lint(34, 9), + ]); + } + + test_staticMethod_referenceToExistingInstanceOfClass() async { + await assertNoDiagnostics(r''' +class A { + static final array = []; + static A staticM(int i) { + return array[i]; + } +} +'''); + } + + test_staticMethod_returnsInstantiatedInstance() async { + await assertNoDiagnostics(r''' +class A { + A.named(); + static A staticM() => A.named(); +} +'''); + } + + test_staticMethod_returnsNullable() async { + await assertNoDiagnostics(r''' +class A { + A.named(); + static A? staticM() => 1 == 1 ? null : A.named(); +} +'''); + } + + test_staticMethod_returnsUnrelatedType() async { + await assertNoDiagnostics(r''' +class A { + A.named(); + static Object staticM() => Object(); + + /*static A? ok2() => 1==1 ? null : A.internal(); // OK*/ +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_contains_test.dart b/pkg/linter/test/rules/prefer_contains_test.dart new file mode 100644 index 00000000000..a12cb410f14 --- /dev/null +++ b/pkg/linter/test/rules/prefer_contains_test.dart @@ -0,0 +1,516 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferContainsTest); + }); +} + +@reflectiveTest +class PreferContainsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_contains'; + + test_argumentTypeNotAssignable() async { + await assertDiagnostics(r''' +List list = []; +condition() { + var next; + while ((next = list.indexOf('{')) != -1) {} +} +''', [ + // No lint + error(CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE, 77, 3), + ]); + } + + test_list_indexOf_greaterThan_negativeOne() async { + await assertDiagnostics(r''' +void f(List list) { + list.indexOf(1) > -1; +} +''', [ + lint(27, 20), + ]); + } + + test_list_indexOf_notEqual_negativeOne() async { + await assertDiagnostics(r''' +void f(List list) { + list.indexOf(1) != -1; +} +''', [ + lint(27, 21), + ]); + } + + test_listConcatenation_indexOfWithDifferentArguments() async { + await assertNoDiagnostics(r''' +void f(List list) { + list.indexOf(1) + "a".indexOf("ab") > 0; +} +'''); + } + + test_listLiteral_indexOf_equalEqual_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) == -1; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_equalEqual_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) == -2; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_equalEqual_zero() async { + await assertNoDiagnostics(r''' +void f() { + [].indexOf(1) == 0; +} +'''); + } + + test_listLiteral_indexOf_greater_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) > -2; +} +''', [ + lint(13, 18), + ]); + } + + test_listLiteral_indexOf_greaterOr_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) >= -2; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_greaterOrEqual_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) >= -1; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_greaterOrEqual_zero() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) >= 0; +} +''', [ + lint(13, 18), + ]); + } + + test_listLiteral_indexOf_greaterThan_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) > -1; +} +''', [ + lint(13, 18), + ]); + } + + test_listLiteral_indexOf_greaterThan_one() async { + await assertNoDiagnostics(r''' +void f() { + [].indexOf(1) > 1; +} +'''); + } + + test_listLiteral_indexOf_greaterThan_zero() async { + await assertNoDiagnostics(r''' +void f() { + [].indexOf(1) > 0; +} +'''); + } + + test_listLiteral_indexOf_lessOrEqual_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) <= -1; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_lessOrEqual_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) <= -2; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_lessOrEqual_zero() async { + await assertNoDiagnostics(r''' +void f() { + [].indexOf(1) <= 0; +} +'''); + } + + test_listLiteral_indexOf_lessThan_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) < -1; +} +''', [ + lint(13, 18), + ]); + } + + test_listLiteral_indexOf_lessThan_negativeOneConst() async { + await assertDiagnostics(r''' +const int MINUS_ONE = -1; +void f() { + [].indexOf(1) < MINUS_ONE; +} +''', [ + lint(39, 25), + ]); + } + + test_listLiteral_indexOf_lessThan_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) < -2; +} +''', [ + lint(13, 18), + ]); + } + + test_listLiteral_indexOf_lessThan_zero() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) < 0; +} +''', [ + lint(13, 17), + ]); + } + + test_listLiteral_indexOf_notEqual_negativeOne() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) != -1; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_notEqual_negativeTwo() async { + await assertDiagnostics(r''' +void f() { + [].indexOf(1) != -2; +} +''', [ + lint(13, 19), + ]); + } + + test_listLiteral_indexOf_notEqual_zero() async { + await assertNoDiagnostics(r''' +void f() { + [].indexOf(1) != 0; +} +'''); + } + + test_listTypedef_indexOf_equalEqual_negativeOne() async { + await assertDiagnostics(r''' +typedef F = List; +void f(F list) { + list.indexOf(1) == -1; +} +''', [ + lint(42, 21), + ]); + } + + test_negativeOne_equalEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 == [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeOne_greaterOrEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 >= [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeOne_greaterThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 > [].indexOf(1); +} +''', [ + lint(13, 18), + ]); + } + + test_negativeOne_lessOrEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 <= [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeOne_lessThan_list_indexOf() async { + await assertDiagnostics(r''' +void f(List list) { + -1 < list.indexOf(1); +} +''', [ + lint(27, 20), + ]); + } + + test_negativeOne_lessThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 < [].indexOf(1); +} +''', [ + lint(13, 18), + ]); + } + + test_negativeOne_notEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -1 != [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeOneConst_lessThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +const int MINUS_ONE = -1; +void f() { + MINUS_ONE < [].indexOf(1); +} +''', [ + lint(39, 25), + ]); + } + + test_negativeTwo_equalEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 == [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeTwo_greaterOrEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 >= [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeTwo_greaterThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 > [].indexOf(1); +} +''', [ + lint(13, 18), + ]); + } + + test_negativeTwo_lessOrEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 <= [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_negativeTwo_lessThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 < [].indexOf(1); +} +''', [ + lint(13, 18), + ]); + } + + test_negativeTwo_notEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + -2 != [].indexOf(1); +} +''', [ + lint(13, 19), + ]); + } + + test_promotedToList_indexOf_lessThan_zero() async { + await assertDiagnostics(r''' +bool f(T list) => + list is List && list.indexOf(1) < 0; +''', [ + lint(44, 19), + ]); + } + + /// https://github.com/dart-lang/linter/issues/3546 + test_secondArgNonZero() async { + await assertNoDiagnostics(r''' +bool b = '11'.indexOf('2', 1) == -1; +'''); + } + + /// https://github.com/dart-lang/linter/issues/3546 + test_secondArgZero() async { + await assertDiagnostics(r''' +bool b = '11'.indexOf('2', 0) == -1; +''', [ + lint(9, 26), + ]); + } + + test_stringLiteral_indexOf_equalEqual_negativeOne() async { + await assertDiagnostics(r''' +void f() { + 'aaa'.indexOf('a') == -1; +} +''', [ + lint(13, 24), + ]); + } + + test_stringLiteral_indexOf_twoArguments() async { + await assertNoDiagnostics(r''' +void f() { + 'aaa'.indexOf('a', 2); +} +'''); + } + + test_typeVariableExtendingList_indexOf_lessThan_zero() async { + await assertDiagnostics(r''' +bool f>(T list) => + list.indexOf(1) < 0; +''', [ + lint(41, 19), + ]); + } + + test_unnecessaryCast() async { + await assertDiagnostics(r''' +bool le3 = ([].indexOf(1) as int) > -1; +''', [ + lint(11, 27), + error(WarningCode.UNNECESSARY_CAST, 12, 20), + ]); + } + + test_zero_equalEqual_listLiteral_indexOf() async { + await assertNoDiagnostics(r''' +void f() { + 0 == [].indexOf(1); +} +'''); + } + + test_zero_greaterOrEqual_listLiteral_indexOf() async { + await assertNoDiagnostics(r''' +void f() { + 0 >= [].indexOf(1); +} +'''); + } + + test_zero_greaterThan_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + 0 > [].indexOf(1); +} +''', [ + lint(13, 17), + ]); + } + + test_zero_lessOrEqual_listLiteral_indexOf() async { + await assertDiagnostics(r''' +void f() { + 0 <= [].indexOf(1); +} +''', [ + lint(13, 18), + ]); + } + + test_zero_lessThan_listLiteral_indexOf() async { + await assertNoDiagnostics(r''' +void f() { + 0 < [].indexOf(1); +} +'''); + } + + test_zero_notEqual_listLiteral_indexOf() async { + await assertNoDiagnostics(r''' +void f() { + 0 != [].indexOf(1); +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_double_quotes_test.dart b/pkg/linter/test/rules/prefer_double_quotes_test.dart new file mode 100644 index 00000000000..cee2f21bede --- /dev/null +++ b/pkg/linter/test/rules/prefer_double_quotes_test.dart @@ -0,0 +1,116 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferDoubleQuotesTest); + }); +} + +@reflectiveTest +class PreferDoubleQuotesTest extends LintRuleTest { + @override + String get lintRule => 'prefer_double_quotes'; + + test_doubleQuotes() async { + await assertNoDiagnostics(r''' +var s = "uses double"; +'''); + } + + test_doubleQuotes_raw() async { + await assertNoDiagnostics(r''' +var s = r"uses double"; +'''); + } + + test_doubleQuotes_triple_raw() async { + await assertNoDiagnostics(r''' +var s = r"""uses double"""; +'''); + } + + test_doubleQuotes_withInterpolation() async { + await assertNoDiagnostics(r''' +var x = "x"; +var s = "uses double $x"; +'''); + } + + test_doubleQuotes_withInterpolationWithSingleQuote() async { + await assertNoDiagnostics(r''' +var x = "x"; +var s = "foo ${x == 'x'} bar"; +'''); + } + + test_singleQuote() async { + await assertDiagnostics(r''' +var s = 'no quote'; +''', [ + lint(8, 10), + ]); + } + + test_singleQuote_hasDoubleQuote_withInterpolation() async { + await assertNoDiagnostics(r''' +var x = "x"; +var s = 'has double quote " $x'; +'''); + } + + test_singleQuote_hasDoubleQuotes() async { + await assertNoDiagnostics(r''' +var s = 'has double quote "'; +'''); + } + + test_singleQuote_raw() async { + await assertDiagnostics(r''' +var s = r'no double quote'; +''', [ + lint(8, 18), + ]); + } + + test_singleQuote_raw_hasDoubleQuotes() async { + await assertNoDiagnostics(r''' +var s = r'has double quote "'; +'''); + } + + test_singleQuote_triple_raw() async { + await assertDiagnostics(r""" +var s = r'''no double quote'''; +""", [ + lint(8, 22), + ]); + } + + test_singleQuote_triple_raw_hasDouble() async { + await assertNoDiagnostics(r""" +var s = r'''has double quote "'''; +"""); + } + + test_singleQuote_withInterpolation() async { + await assertDiagnostics(r''' +var x = "x"; +var s = 'no double quote $x'; +''', [ + lint(21, 20), + ]); + } + + test_singleQuote_withInterpolationWithDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = "x"; +var s = 'foo ${x == "x"} bar'; +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_expression_function_bodies_test.dart b/pkg/linter/test/rules/prefer_expression_function_bodies_test.dart new file mode 100644 index 00000000000..60293a5c94d --- /dev/null +++ b/pkg/linter/test/rules/prefer_expression_function_bodies_test.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferExpressionFunctionBodiesTest); + }); +} + +@reflectiveTest +class PreferExpressionFunctionBodiesTest extends LintRuleTest { + @override + String get lintRule => 'prefer_expression_function_bodies'; + + /// https://github.com/dart-lang/linter/issues/4221 + test_voidReturn() async { + await assertNoDiagnostics(r''' +class C { + void f() { + return; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_final_fields_test.dart b/pkg/linter/test/rules/prefer_final_fields_test.dart new file mode 100644 index 00000000000..1c4e154c0db --- /dev/null +++ b/pkg/linter/test/rules/prefer_final_fields_test.dart @@ -0,0 +1,198 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFinalFieldsTest); + defineReflectiveTests(PreferFinalFieldsExtensionTypesTest); + }); +} + +@reflectiveTest +class PreferFinalFieldsExtensionTypesTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'prefer_final_fields'; + + test_field_instance() async { + await assertDiagnostics(r''' +extension type E(Object o) { + int _i = 0; +} +''', [ + // No Lint. + error(CompileTimeErrorCode.EXTENSION_TYPE_DECLARES_INSTANCE_FIELD, 35, 2), + error(WarningCode.UNUSED_FIELD, 35, 2), + ]); + } + + test_field_static() async { + await assertDiagnostics(r''' +extension type E(Object o) { + static int _i = 0; +} +''', [ + error(WarningCode.UNUSED_FIELD, 42, 2), + lint(42, 6), + ]); + } + + test_field_static_writtenInConstructor() async { + await assertDiagnostics(r''' +extension type E(Object o) { + static Object _o = 0; + E.e(this.o) { + _o = o; + } +} +''', [ + // No lint. + error(WarningCode.UNUSED_FIELD, 45, 2), + ]); + } +} + +@reflectiveTest +class PreferFinalFieldsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_final_fields'; + + test_assignedInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; +int f(C c) { + c._x = 1; + return c._x; +} +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; +class C { + int _x = 0; +} +'''); + } + + test_declaredInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; +class C { + int _x = 0; +} +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; +int f(C c) { + c._x = 1; + return c._x; +} +'''); + } + + test_enum() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + int _x = 0; + int get x => _x; +} +''', [ + // No Lint. + error(CompileTimeErrorCode.NON_FINAL_FIELD_IN_ENUM, 24, 2), + ]); + } + + test_overrideField_extends() async { + await assertNoDiagnostics(r''' +class A { + bool _a = false; + void m() { + _a = true; + print(_a); + } +} + +class B extends A { + @override + bool _a = false; + + @override + void m() { + print(_a); + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3760 + test_overrideField_implements() async { + await assertNoDiagnostics(r''' +class A { + bool _a = false; + void m() { + _a = true; + print(_a); + } +} + +class B implements A { + @override + bool _a = false; + + @override + void m() { + print(_a); + } +} +'''); + } + + test_overrideSetter_extends() async { + await assertNoDiagnostics(r''' +class A { + set _a(bool a) {} + void m() { + _a = true; + } +} + +class B extends A { + @override + bool _a = false; + + @override + void m() { + print(_a); + } +} +'''); + } + + test_overrideSetter_implements() async { + await assertNoDiagnostics(r''' +class A { + set _a(bool a) {} + void m() { + _a = true; + } +} + +class B implements A { + @override + bool _a = false; + + @override + void m() { + print(_a); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_final_in_for_each_test.dart b/pkg/linter/test/rules/prefer_final_in_for_each_test.dart new file mode 100644 index 00000000000..b7764b9fbf5 --- /dev/null +++ b/pkg/linter/test/rules/prefer_final_in_for_each_test.dart @@ -0,0 +1,202 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFinalInForEachTestLanguage300); + }); +} + +@reflectiveTest +class PreferFinalInForEachTestLanguage300 extends LintRuleTest { + @override + String get lintRule => 'prefer_final_in_for_each'; + + test_int() async { + await assertDiagnostics(r''' +f() { + for (var i in [1, 2, 3]) { } +} +''', [ + lint(17, 1), + ]); + } + + test_int_final_ok() async { + await assertNoDiagnostics(r''' +f() { + for (final i in [1, 2, 3]) { } +} +'''); + } + + test_int_mutated_ok() async { + await assertNoDiagnostics(r''' +f() { + for (var i in [1, 2, 3]) { + i += 1; + } +} +'''); + } + + test_list() async { + await assertDiagnostics(r''' +f() { + for (var [i, j] in [[1, 2]]) { } +} +''', [ + lint(17, 6), + ]); + } + + test_list_final() async { + await assertNoDiagnostics(r''' +f() { + for (final [i, j] in [[1, 2]]) { } +} +'''); + } + + test_list_mutated() async { + await assertNoDiagnostics(r''' +f() { + for (var [i, j] in [[1, 2]]) { + i += 2; + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4353 + test_listLiteral_forEach() async { + await assertDiagnostics(r''' +List f() => [ + for (var i in [1, 2]) i + 3 + ]; +''', [ + lint(34, 1), + ]); + } + + test_listLiteral_forEach_mutated() async { + await assertNoDiagnostics(r''' +List f() => [ + for (var i in [1, 2]) i += 3 + ]; +'''); + } + + test_map() async { + await assertDiagnostics(r''' +f() { + for (var {'i' : j} in [{'i' : 1}]) { } +} +''', [ + lint(17, 9), + ]); + } + + test_map_final() async { + await assertNoDiagnostics(r''' +f() { + for (final {'i' : j} in [{'i' : 1}]) { } +} +'''); + } + + test_map_mutated() async { + await assertNoDiagnostics(r''' +f() { + for (var {'i' : j} in [{'i' : 1}]) { + j += 2; + } +} +'''); + } + + test_object() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + for (var A(:a) in [A(1)]) { } +} +''', [ + lint(52, 5), + ]); + } + + test_object_final() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + for (final A(:a) in [A(1)]) { } +} +'''); + } + + test_object_mutated() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + for (var A(:a) in [A(1)]) { + a += 2; + } +} +'''); + } + + test_outOfLoopDeclaration_ok() async { + await assertNoDiagnostics(r''' +f() { + int j; + for (j in [1, 2, 3]) { } +} +'''); + } + + test_record() async { + await assertDiagnostics(r''' +f() { + for (var (i, j) in [(1, 2)]) { } +} +''', [ + lint(17, 6), + ]); + } + + test_record_final() async { + await assertNoDiagnostics(r''' +f() { + for (final (i, j) in [(1, 2)]) { } +} +'''); + } + + test_record_mutated() async { + await assertNoDiagnostics(r''' +f() { + for (var (int i, j) in [(1, 2)]) { + i++; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_final_locals_test.dart b/pkg/linter/test/rules/prefer_final_locals_test.dart new file mode 100644 index 00000000000..614ae69ec45 --- /dev/null +++ b/pkg/linter/test/rules/prefer_final_locals_test.dart @@ -0,0 +1,440 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFinalLocalsTest); + }); +} + +@reflectiveTest +class PreferFinalLocalsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_final_locals'; + + test_destructured_listPattern() async { + await assertDiagnostics(r''' +f() { + var [a, b] = ['a', 'b']; +} +''', [ + lint(8, 3), + ]); + } + + test_destructured_listPattern_final() async { + await assertNoDiagnostics(r''' +f() { + final [a, b] = [1, 2]; +} +'''); + } + + test_destructured_listPattern_mutated() async { + await assertNoDiagnostics(r''' +f() { + var [a, b] = [1, 2]; + ++a; +} +'''); + } + + test_destructured_listPatternWithRest() async { + await assertDiagnostics(r''' +f() { + var [a, b, ...rest] = [1, 2, 3, 4, 5, 6, 7]; +} +''', [ + lint(8, 3), + ]); + } + + test_destructured_listPatternWithRest_mutated() async { + await assertNoDiagnostics(r''' +f() { + var [a, b, ...rest] = [1, 2, 3, 4, 5, 6, 7]; + ++a; +} +'''); + } + + test_destructured_mapPattern() async { + await assertDiagnostics(r''' +f() { + var {'first': a, 'second': b} = {'first': 1, 'second': 2}; +} +''', [ + lint(8, 3), + ]); + } + + test_destructured_mapPattern_final() async { + await assertNoDiagnostics(r''' +f() { + final {'first': a, 'second': b} = {'first': 1, 'second': 2}; +} +'''); + } + + test_destructured_mapPattern_mutated() async { + await assertNoDiagnostics(r''' +f() { + var {'first': a, 'second': b} = {'first': 1, 'second': 2}; + ++a; +} +'''); + } + + test_destructured_objectPattern() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + var A(a: b) = A(1); +} +''', [ + lint(42, 3), + ]); + } + + test_destructured_objectPattern_final() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + final A(a: b) = A(1); +} +'''); + } + + test_destructured_objectPattern_mutated() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} +f() { + var A(a: b) = A(1); + ++b; +} +'''); + } + + test_destructured_recordPattern() async { + await assertDiagnostics(r''' +f() { + var (a, b) = ('a', 'b'); +} +''', [ + lint(8, 3), + ]); + } + + test_destructured_recordPattern_final() async { + await assertNoDiagnostics(r''' +f() { + final (a, b) = ('a', 'b'); +} +'''); + } + + test_destructured_recordPattern_forLoop() async { + await assertDiagnostics(r''' +f() { + for (var (a, b) in [(1, 2)]) { } +} +''', [ + lint(18, 1), + lint(21, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4286 + test_destructured_recordPattern_forLoop_final() async { + await assertNoDiagnostics(r''' +f() { + for (final (a, b) in [(1, 2), (3, 4), (5, 6)]) { } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4539 + test_destructured_recordPattern_forLoop_mutated() async { + await assertNoDiagnostics(r''' +f() { + for (var (a, b) in [(1, 2)]) { + ++a; + } +} +'''); + } + + test_destructured_recordPattern_mutated() async { + await assertNoDiagnostics(r''' +f() { + var (a, b) = (1, 'b'); + ++a; +} +'''); + } + + test_destructured_recordPattern_withParenthesizedPattern() async { + await assertDiagnostics(r''' +f() { + var ((a, b)) = ('a', 'b'); +} +''', [ + lint(8, 3), + ]); + } + + test_field() async { + await assertNoDiagnostics(r''' +class C { + int f = 0; +} +'''); + } + + test_ifPatternList() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case [int x, final int y]) x; +} +''', [ + lint(28, 5), + ]); + } + + test_ifPatternList_final() async { + await assertNoDiagnostics(r''' +f(Object o) { + if (o case [final int x, final int y]) x; +} +'''); + } + + test_ifPatternMap() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case {'x': var x}) print('$x'); +} +''', [ + lint(33, 5), + ]); + } + + test_ifPatternMap_final() async { + await assertNoDiagnostics(r''' +f(Object o) { + if (o case {'x': final x}) x; +} +'''); + } + + test_ifPatternObject() async { + await assertDiagnostics(r''' +class C { + int c; + C(this.c); +} + +f(Object o) { + if (o case C(c: var x)) x; +} +''', [ + lint(67, 5), + ]); + } + + test_ifPatternObject_final() async { + await assertNoDiagnostics(r''' +class C { + int c; + C(this.c); +} + +f(Object o) { + if (o case C(c: final x)) x; +} +'''); + } + + test_ifPatternRecord() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case (int x, int y)) x; +} +''', [ + lint(28, 5), + lint(35, 5), + ]); + } + + test_ifPatternRecord_final() async { + await assertNoDiagnostics(r''' +f(Object o) { + if (o case (final int x, final int y)) x; +} +'''); + } + + test_nonDeclaration_destructured_recordPattern() async { + await assertNoDiagnostics(r''' +f(String a, String b) { + [a, b] = ['a', 'b']; +} +'''); + } + + test_notReassigned_withFinal() async { + await assertNoDiagnostics(r''' +void f() { + final a = 'hello'; + print(a); +} +'''); + } + + test_notReassigned_withType_multiple() async { + await assertDiagnostics(r''' +void f() { + String a = 'hello', b = 'world'; + print(a); + print(b); +} +''', [ + lint(13, 6), + ]); + } + + test_notReassigned_withVar() async { + await assertDiagnostics(r''' +void f() { + var a = ''; + print(a); +} +''', [ + lint(13, 3), + ]); + } + + test_notReassigned_withVar_multiple() async { + await assertDiagnostics(r''' +void f() { + var a = 'hello', b = 'world'; + print(a); + print(b); +} +''', [ + lint(13, 3), + ]); + } + + test_reassigned() async { + await assertNoDiagnostics(r''' +void f() { + var a = 'hello'; + a = 'hello world'; +} +'''); + } + + test_reassigned_multiple() async { + await assertNoDiagnostics(r''' +void f() { + var a = 'hello', b = 'world'; + a = 'world'; +} +'''); + } + + test_switch_objectPattern() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && var b): b; + } +} +''', [ + lint(79, 5), + ]); + } + + test_switch_objectPattern_final() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && final b): b; + } +} +'''); + } + + test_switch_objectPattern_mutated() async { + await assertNoDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && var b): ++b; + } +} +'''); + } + + test_switch_recordPattern() async { + await assertDiagnostics(r''' +f() { + switch ((1, 2)) { + case (var a, int b): a; + } +} +''', [ + lint(36, 5), + lint(43, 5), + ]); + } + + test_switch_recordPattern_final() async { + await assertNoDiagnostics(r''' +f() { + switch ((1, 2)) { + case (final a, final int b): a; + } +} +'''); + } + + test_switch_recordPattern_mutated() async { + await assertNoDiagnostics(r''' +f() { + switch ((1, 2)) { + case (var a, final int b): ++a; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_final_parameters_test.dart b/pkg/linter/test/rules/prefer_final_parameters_test.dart new file mode 100644 index 00000000000..1bf42fc0823 --- /dev/null +++ b/pkg/linter/test/rules/prefer_final_parameters_test.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFinalParametersTest); + }); +} + +@reflectiveTest +class PreferFinalParametersTest extends LintRuleTest { + @override + String get lintRule => 'prefer_final_parameters'; + + test_listPattern_destructured() async { + await assertNoDiagnostics(''' +void f(int p) { + [_, p, _] = [1, 2, 3]; +} +'''); + } + + test_recordPattern_destructured() async { + await assertNoDiagnostics(r''' +void f(int a, int b) { + (a, b) = (1, 2); +} +'''); + } + + test_superParameter() async { + await assertDiagnostics(''' +class D { + D(final int superParameter); +} + +class E extends D { + E(super.superParameter); // OK +} +''', []); + } + + test_superParameter_optional() async { + await assertDiagnostics(''' +class A { + final String? a; + + A({this.a}); +} + +class B extends A { + B({super.a}); // OK +} +''', []); + } +} diff --git a/pkg/linter/test/rules/prefer_for_elements_to_map_fromIterable_test.dart b/pkg/linter/test/rules/prefer_for_elements_to_map_fromIterable_test.dart new file mode 100644 index 00000000000..d4a4f866d7b --- /dev/null +++ b/pkg/linter/test/rules/prefer_for_elements_to_map_fromIterable_test.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: file_names + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferForElementsToMapFromIterableTest); + }); +} + +@reflectiveTest +class PreferForElementsToMapFromIterableTest extends LintRuleTest { + // TODO(srawlins): Add tests with block-bodied closures. + + @override + String get lintRule => 'prefer_for_elements_to_map_fromIterable'; + + test_hasKeyAndValue_closuresAreSimple() async { + await assertDiagnostics(r''' +void f(Iterable i) { + Map.fromIterable(i, key: (k) => k * 2, value: (v) => 0); +} +''', [ + lint(28, 55), + ]); + } + + test_hasKeyAndValue_closuresReferenceE() async { + await assertDiagnostics(r''' +void f(Iterable i, int e) { + Map.fromIterable(i, key: (k) => k * e, value: (v) => v + e); +} +''', [ + lint(35, 59), + ]); + } + + test_hasKeyAndValue_closuresShadowVariable() async { + await assertDiagnostics(r''' +void f(Iterable i, int k) { + Map.fromIterable(i, key: (k) => k * 2, value: (v) => k); +} +''', [ + lint(35, 55), + ]); + } + + test_missingKey() async { + await assertNoDiagnostics(r''' +void f(Iterable i) { + Map.fromIterable(i, value: (e) => e + 3); +} +'''); + } + + test_nonMap_fromIterable() async { + await assertNoDiagnostics(r''' +void f(Iterable i) { + A.fromIterable(i, key: (e) => e * 2, value: (e) => e + 3); +} + +class A { + A.fromIterable( + Iterable i, {K Function(int)? key, V Function(int)? value}); +} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_generic_function_type_aliases_test.dart b/pkg/linter/test/rules/prefer_generic_function_type_aliases_test.dart new file mode 100644 index 00000000000..345d0443458 --- /dev/null +++ b/pkg/linter/test/rules/prefer_generic_function_type_aliases_test.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferGenericFunctionTypeAliasesTest); + }); +} + +@reflectiveTest +class PreferGenericFunctionTypeAliasesTest extends LintRuleTest { + @override + String get lintRule => 'prefer_generic_function_type_aliases'; + + test_classicTypedef() async { + await assertDiagnostics(r''' +typedef void F(); +''', [ + lint(13, 1), + ]); + } + + test_genericFunctionType() async { + await assertNoDiagnostics(r''' +typedef F = void Function(); +'''); + } + + /// https://github.com/dart-lang/linter/issues/2777 + test_undefinedFunction() async { + await assertDiagnostics(r''' +typedef Cb2 +''', [ + // No lint + error(ParserErrorCode.EXPECTED_TOKEN, 8, 3), + error(ParserErrorCode.MISSING_TYPEDEF_PARAMETERS, 12, 0), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_if_elements_to_conditional_expressions_test.dart b/pkg/linter/test/rules/prefer_if_elements_to_conditional_expressions_test.dart new file mode 100644 index 00000000000..94a92c139ee --- /dev/null +++ b/pkg/linter/test/rules/prefer_if_elements_to_conditional_expressions_test.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + // TODO(srawlins): Add test with conditional inside if-element. + defineReflectiveTests(PreferIfElementsToConditionalExpressionsTest); + }); +} + +@reflectiveTest +class PreferIfElementsToConditionalExpressionsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_if_elements_to_conditional_expressions'; + + test_conditionalInList() async { + await assertDiagnostics(r''' +List f(bool b) { + return ['a', b ? 'c' : 'd', 'e']; +} +''', [ + lint(40, 13), + ]); + } + + test_conditionalInList_parenthesized() async { + await assertDiagnostics(r''' +List f(bool b) { + return ['a', (b ? 'c' : 'd'), 'e']; +} +''', [ + lint(40, 15), + ]); + } + + test_conditionalInMap() async { + await assertNoDiagnostics(r''' +Map f(bool b) { + return {'a': 1, b ? 'c' : 'd' : 2, 'e': 3}; +} +'''); + } + + test_conditionalInSet() async { + await assertDiagnostics(r''' +Set f(bool b) { + return {'a', b ? 'c' : 'd', 'e'}; +} +''', [ + lint(39, 13), + ]); + } + + test_conditionalInSet_parenthesizedTwice() async { + await assertDiagnostics(r''' +Set f(bool b) { + return {'a', ((b ? 'c' : 'd')), 'e'}; +} +''', [ + lint(39, 17), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_inlined_adds_test.dart b/pkg/linter/test/rules/prefer_inlined_adds_test.dart new file mode 100644 index 00000000000..ed146d79fd9 --- /dev/null +++ b/pkg/linter/test/rules/prefer_inlined_adds_test.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferInlinedAddsTest); + }); +} + +@reflectiveTest +class PreferInlinedAddsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_inlined_adds'; + + test_listLiteral_addAll_nonListLiteral() async { + await assertNoDiagnostics(r''' +var x; +var y = ['a']..addAll(x ?? const []); +'''); + } + + test_listLiteral_cascadeAdd() async { + await assertDiagnostics(r''' +var x = ['a']..add('b'); +''', [ + lint(15, 3), + ]); + } + + test_listLiteral_cascadeAdd_multiple() async { + await assertDiagnostics(r''' +var x = ['a']..add('b')..add('c'); +''', [ + lint(15, 3), + ]); + } + + test_listLiteral_cascadeAddAll_listLiteral() async { + await assertDiagnostics(r''' +var x = ['a']..addAll(['b', 'c']); +''', [ + lint(15, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_interpolation_to_compose_strings_test.dart b/pkg/linter/test/rules/prefer_interpolation_to_compose_strings_test.dart new file mode 100644 index 00000000000..f5386fc9e58 --- /dev/null +++ b/pkg/linter/test/rules/prefer_interpolation_to_compose_strings_test.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/rules/prefer_interpolation_to_compose_strings.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferInterpolationToComposeStringsTest); + }); +} + +@reflectiveTest +class PreferInterpolationToComposeStringsTest extends LintRuleTest { + @override + String get lintRule => PreferInterpolationToComposeStrings.code.name; + + test_stringLiteral_stringLiteral() async { + await assertNoDiagnostics(r''' +final a = 'foo' + 'bar'; +'''); + } + + test_stringLiteral_toStringInvocation() async { + await assertDiagnostics(r''' +final a = A(); +final b = 'foo' + a.toString(); + +class A { + @override + String toString({int? x}) => 'A'; +} +''', [ + lint(25, 20), + ]); + } + + test_stringLiteral_toStringInvocation_withArguments() async { + await assertNoDiagnostics(r''' +final a = A(); +final b = 'foo' + a.toString(x: 0); + +class A { + @override + String toString({int? x}) => 'A'; +} +'''); + } + + test_stringLiteral_variableString() async { + await assertDiagnostics(r''' +final a = 'foo'; +final b = 'bar' + a; +''', [ + lint(27, 9), + ]); + } + + /// #2490 + test_stringLiteralRaw_variableString() async { + await assertNoDiagnostics(r''' +final a = 'foo'; +final b = r'bar' + a; +'''); + } + + test_toStringInvocation_stringLiteral_withArguments() async { + await assertNoDiagnostics(r''' +final a = A(); +final b = a.toString(x: 0) + 'foo'; + +class A { + @override + String toString({int? x}) => 'A'; +} +'''); + } + + /// #792 + test_variableNotString() async { + await assertNoDiagnostics(r''' +class A { + A operator +(String other) => this; +} + +void f(A a) { + a + ' '; +} +'''); + } + + /// #813 + test_variableString_plusEqual_stringLiteral() async { + await assertNoDiagnostics(r''' +void f() { + var a = 'foo'; + a += 'bar'; + a; +} +'''); + } + + test_variableString_stringLiteral() async { + await assertDiagnostics(r''' +final a = 'foo'; +final b = a + 'bar'; +''', [ + lint(27, 9), + ]); + } + + /// #2490 + test_variableString_stringLiteralRaw() async { + await assertNoDiagnostics(r''' +final a = 'foo'; +final b = a + r'bar'; +'''); + } + + /// #735 + test_variableString_variableString() async { + await assertNoDiagnostics(r''' +final a = 'foo'; +final b = 'bar'; +final c = a + b; +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_iterable_whereType_test.dart b/pkg/linter/test/rules/prefer_iterable_whereType_test.dart new file mode 100644 index 00000000000..fb391e18bdd --- /dev/null +++ b/pkg/linter/test/rules/prefer_iterable_whereType_test.dart @@ -0,0 +1,138 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +// ignore_for_file: file_names + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferIterableWhereTypeTest); + }); +} + +@reflectiveTest +class PreferIterableWhereTypeTest extends LintRuleTest { + @override + String get lintRule => 'prefer_iterable_whereType'; + + test_closureWithIs() async { + await assertDiagnostics(r''' +var x = [42].where((e) => e is String); +''', [ + lint(13, 5), + ]); + } + + test_closureWithIs_blockBodySingleStatement() async { + await assertDiagnostics(r''' +var x = [42].where( + (e) { + return e is String; +}); +''', [ + lint(13, 5), + ]); + } + + test_closureWithIs_multipleStatements() async { + await assertNoDiagnostics(r''' +var x = [42].where((e) { + print(''); + return e is String; +}); +'''); + } + + test_closureWithIs_parenthesized() async { + await assertDiagnostics(r''' +var x = [42].where((e) => (e is String)); +''', [ + lint(13, 5), + ]); + } + + test_closureWithIs_wrongSimpleTarget() async { + await assertNoDiagnostics(r''' +var l = [42]; +var x = l.where((e) => l is String); +'''); + } + + test_closureWithIs_wrongTarget() async { + await assertNoDiagnostics(r''' +var x = [42].where((e) => e.isEven is String); +'''); + } + + test_closureWithIsNot() async { + await assertNoDiagnostics(r''' +var x = [42, 42.5].where((e) => e is! int); +'''); + } + + test_functionReference() async { + await assertNoDiagnostics(r''' +bool p(Object e) => false; +var x = [42].where(p); +'''); + } + + test_nonIterable_whereMethod_closure_explicitTarget() async { + await assertNoDiagnostics(r''' +class A { + void where(bool Function(Object) g) {} +} + +void f(A a) { + a.where((e) => e is String); +} +'''); + } + + test_nonIterable_whereMethod_closure_implicitTarget() async { + await assertNoDiagnostics(r''' +class A { + void where(bool Function(Object) f) {} + + void m(A a) { + a.where((e) => e is String); + } +} + +'''); + } + + test_nonIterable_whereMethod_explicitTarget() async { + await assertNoDiagnostics(r''' +class A { + bool where() => true; +} + +void f(A a) { + a.where(); +} +'''); + } + + test_nonIterable_whereMethod_implicitTarget() async { + await assertNoDiagnostics(r''' +class A { + bool where() => true; + + void m() { + where(); + } +} +'''); + } + + test_whereType() async { + await assertNoDiagnostics(r''' +var x = [42].whereType(); +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_mixin_test.dart b/pkg/linter/test/rules/prefer_mixin_test.dart new file mode 100644 index 00000000000..d96f91609fe --- /dev/null +++ b/pkg/linter/test/rules/prefer_mixin_test.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferMixinTestLanguage219); + defineReflectiveTests(PreferMixinTest); + }); +} + +abstract class BasePreferMixinTest extends LintRuleTest { + @override + String get lintRule => 'prefer_mixin'; + + test_legacyCoreClasses() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; +import 'dart:convert'; + +abstract class I with IterableMixin {} +abstract class L with ListMixin {} +abstract class MM with MapMixin {} +abstract class S with SetMixin {} +abstract class SCS with StringConversionSinkMixin {} +'''); + } + + test_mixedInMixin_ok() async { + await assertNoDiagnostics(r''' +mixin M {} + +class C with M {} +'''); + } + + test_mixedInTypeAlias_ok() async { + await assertNoDiagnostics(r''' +mixin M {} + +typedef AAA = M; + +abstract class CCC with AAA { } +'''); + } +} + +@reflectiveTest +class PreferMixinTest extends BasePreferMixinTest { + /// https://github.com/dart-lang/linter/issues/4065 + test_mixinClass() async { + await assertNoDiagnostics(r''' +mixin class M { } + +class Z with M { } +'''); + } +} + +@reflectiveTest +class PreferMixinTestLanguage219 extends BasePreferMixinTest + with LanguageVersion219Mixin { + test_mixedInClass() async { + await assertDiagnostics(r''' +class A {} + +class B extends Object with A {} +''', [ + lint(40, 1), + ]); + } + + test_mixedInClass_typAlias() async { + await assertDiagnostics(r''' +class A {} + +typedef AA = A; + +abstract class CC with AA { } +''', [ + lint(52, 2), + ]); + } + + test_mixedInIterableMixin() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; +abstract class C with IterableMixin {} +'''); + } + + test_mixedInListMixin() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; +abstract class C with ListMixin {} +'''); + } + + test_mixedInMaoMixin() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; +abstract class C with MapMixin {} +'''); + } + + test_mixedInMixin() async { + await assertNoDiagnostics(r''' +mixin M {} + +class C with M {} +'''); + } + + test_mixedInSetMixin() async { + await assertNoDiagnostics(r''' +import 'dart:collection'; +abstract class C with SetMixin {} +'''); + } + + test_mixedInStringConversionSinkMixin() async { + await assertNoDiagnostics(r''' +import 'dart:convert'; +abstract class C with StringConversionSinkMixin {} +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_null_aware_method_calls_test.dart b/pkg/linter/test/rules/prefer_null_aware_method_calls_test.dart new file mode 100644 index 00000000000..439001d421a --- /dev/null +++ b/pkg/linter/test/rules/prefer_null_aware_method_calls_test.dart @@ -0,0 +1,103 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferNullAwareMethodCallsTest); + }); +} + +@reflectiveTest +class PreferNullAwareMethodCallsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_null_aware_method_calls'; + + test_conditional_differentTargets() async { + await assertNoDiagnostics(r''' +Function()? f1, f2; +void f() { + f1 != null ? f2!() : null; +} +'''); + } + + test_conditional_propertyAccess_differentProperties() async { + await assertNoDiagnostics(r''' +void f(dynamic p) { + p.a != null ? p.m!() : null; +} +'''); + } + + test_conditional_propertyAccess_sameProperty() async { + await assertDiagnostics(r''' +void f(dynamic p) { + p.m != null ? p.m!() : null; +} +''', [ + lint(36, 6), + ]); + } + + test_conditional_sameTarget() async { + await assertDiagnostics(r''' +Function()? func; +void f() { + func != null ? func!() : null; +} +''', [ + lint(46, 7), + ]); + } + + test_ifNotNull_propertyAccess_differentProperties() async { + await assertNoDiagnostics(r''' +void f(dynamic p) { + if (p.a != null) { + p.m!(); + } +} +'''); + } + + test_ifNotNull_propertyAccess_sameProperty() async { + await assertDiagnostics(r''' +void f(dynamic p) { + if (p.m != null) { + p.m!(); + } +} +''', [ + lint(45, 6), + ]); + } + + test_ifNotNull_sameTarget_blockStatement() async { + await assertDiagnostics(r''' +Function()? func; +void f() { + if (func != null) { + func!(); + } +} +''', [ + lint(55, 7), + ]); + } + + test_ifNotNull_sameTarget_expressionStatement() async { + await assertDiagnostics(r''' +Function()? func; +void f() { + if (func != null) func!(); +} +''', [ + lint(49, 7), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_null_aware_operators_test.dart b/pkg/linter/test/rules/prefer_null_aware_operators_test.dart new file mode 100644 index 00000000000..82cb44f605b --- /dev/null +++ b/pkg/linter/test/rules/prefer_null_aware_operators_test.dart @@ -0,0 +1,248 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferNullAwareOperatorsTest); + }); +} + +@reflectiveTest +class PreferNullAwareOperatorsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_null_aware_operators'; + + test_identifierEqualEqualNull_null_elseBinaryExpression() async { + await assertNoDiagnostics(r''' +void f(A? a) { + a == null ? null : a.b + 10; +} + +abstract class A { + int get b; +} +'''); + } + + test_identifierEqualEqualNull_null_elseMethodCall() async { + await assertDiagnostics(r''' +void f(A? a) { + a == null ? null : a.b(); +} + +abstract class A { + void b(); +} +''', [ + lint(17, 24), + ]); + } + + test_identifierEqualEqualNull_null_elsePrefixedIdentifier() async { + await assertDiagnostics(r''' +void f(A? a) { + a == null ? null : a.b; +} + +abstract class A { + int get b; +} +''', [ + lint(17, 22), + ]); + } + + test_identifierEqualEqualNull_null_elsePropertyAccess() async { + await assertDiagnostics(r''' +void f(A? a) { + a == null ? null : a.b.c; +} + +abstract class A { + A get b; + int get c; +} +''', [ + lint(17, 24), + ]); + } + + test_identifierEqualEqualNull_unrelatedBranch() async { + // This is covered by another rule. + await assertNoDiagnostics(r''' +void f(int? a, int b) { + a == null ? b : a; +} +'''); + } + + test_identifierEqualEqualNull_unrelatedBranches() async { + await assertNoDiagnostics(r''' +void f(int? a, int b) { + a == null ? b.isEven : null; +} +'''); + } + + test_identifierNotEqualNull_prefixedIdentifier_elseNull() async { + await assertDiagnostics(r''' +void f(A? a) { + a != null ? a.b : null; +} + +abstract class A { + int get b; +} +''', [ + lint(17, 22), + ]); + } + + test_identifierNotEqualNull_prefixedIdentifier_null() async { + await assertDiagnostics(r''' +void f(A? a) { + a != null ? a.b : null; +} + +abstract class A { + int get b; +} +''', [ + lint(17, 22), + ]); + } + + test_nullEqualEqualIdentifier_null_elseMethodInvocation() async { + await assertDiagnostics(r''' +void f(A? a) { + null == a ? null : a.b(); +} + +abstract class A { + void b(); +} +''', [ + lint(17, 24), + ]); + } + + test_nullEqualEqualIdentifier_null_elsePrefixedIdentifier() async { + await assertDiagnostics(r''' +void f(A? a) { + null == a ? null : a.b; +} + +abstract class A { + int get b; +} +''', [ + lint(17, 22), + ]); + } + + test_nullEqualEqualPrefixedIdentifier_null_elsePropertyAccess() async { + await assertDiagnostics(r''' +void f(A a) { + null == a.b ? null : a.b!.c; +} + +abstract class A { + A? get b; + int get c; +} +''', [ + lint(16, 27), + ]); + } + + test_nullNotEqualIdentifier_prefixedIdentifier_elseNull() async { + await assertDiagnostics(r''' +void f(A? a) { + null != a ? a.b : null; +} + +abstract class A { + int get b; +} +''', [ + lint(17, 22), + ]); + } + + test_nullNotEqualPrefixedIdentifier_propertyAccess_elseNull() async { + await assertDiagnostics(r''' +void f(A a) { + null != a.b ? a.b!.c : null; +} + +abstract class A { + A? get b; + int get c; +} +''', [ + lint(16, 27), + ]); + } + + test_prefixedIdentifierEqualEqualNull_null_elseMethodInvocation() async { + await assertDiagnostics(r''' +void f(A a) { + a.b == null ? null : a.b!.c(); +} + +abstract class A { + A? get b; + void c(); +} +''', [ + lint(16, 29), + ]); + } + + test_prefixedIdentifierEqualEqualNull_null_elsePropertyAccess() async { + await assertDiagnostics(r''' +void f(A a) { + a.b == null ? null : a.b!.c; +} + +abstract class A { + A? get b; + int get c; +} +''', [ + lint(16, 27), + ]); + } + + test_prefixedIdentifierNotEqualNull_prefixedIdentifier_elseNull() async { + await assertNoDiagnostics(r''' +void f(A a) { + a.b != null ? a.b : null; +} + +abstract class A { + int? get b; +} +'''); + } + + test_prefixedIdentifierNotEqualNull_propertyAccess_elseNull() async { + await assertDiagnostics(r''' +void f(A a) { + a.b != null ? a.b!.c : null; +} + +abstract class A { + A? get b; + int get c; +} +''', [ + lint(16, 27), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_relative_imports_test.dart b/pkg/linter/test/rules/prefer_relative_imports_test.dart new file mode 100644 index 00000000000..28b3d8cedf6 --- /dev/null +++ b/pkg/linter/test/rules/prefer_relative_imports_test.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferRelativeImportsTest); + }); +} + +@reflectiveTest +class PreferRelativeImportsTest extends LintRuleTest { + @override + String get lintRule => 'prefer_relative_imports'; + + @override + bool get addJsPackageDep => true; + + test_externalPackage() async { + await assertNoDiagnostics(r''' +/// This provides [JS]. +import 'package:js/js.dart'; +'''); + } + + test_samePackage_packageSchema() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + await assertDiagnostics(r''' +/// This provides [C]. +import 'package:test/lib.dart'; +''', [ + lint(30, 23), + ]); + } + + test_samePackage_packageSchema_fromOutsideLib() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + var bin = newFile('$testPackageRootPath/bin/bin.dart', r''' +/// This provides [C]. +import 'package:test/lib.dart'; +'''); + await resolveTestFile(); + var result = await resolveFile(bin.path); + await assertNoDiagnosticsIn(result.errors); + } + + test_samePackage_relativeUri() async { + newFile('$testPackageLibPath/lib.dart', r''' +class C {} +'''); + await assertNoDiagnostics(r''' +/// This provides [C]. +import 'lib.dart'; +'''); + } + + test_internalPackage() async { + var packageConfigBuilder = PackageConfigFileBuilder(); + packageConfigBuilder.add( + name: 'internal_package', + rootPath: '$testPackageRootPath/vendor/internal_package', + ); + writeTestPackageConfig(packageConfigBuilder); + + newFile('$testPackageRootPath/vendor/internal_package/lib/lib.dart', r''' +class C {} +'''); + await assertNoDiagnostics(r''' +/// This provides [C]. +import 'package:internal_package/lib.dart'; +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_single_quotes_test.dart b/pkg/linter/test/rules/prefer_single_quotes_test.dart new file mode 100644 index 00000000000..dcb54cc53dc --- /dev/null +++ b/pkg/linter/test/rules/prefer_single_quotes_test.dart @@ -0,0 +1,131 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferSingleQuotesTest); + }); +} + +@reflectiveTest +class PreferSingleQuotesTest extends LintRuleTest { + @override + String get lintRule => 'prefer_single_quotes'; + + test_doubleQuotes() async { + await assertDiagnostics(r''' +var x = "no quote"; +''', [ + lint(8, 10), + ]); + } + + test_doubleQuotes_import() async { + await assertDiagnostics(r''' +import "dart:core"; +''', [ + lint(7, 11), + ]); + } + + test_doubleQuotes_innerSingleQuote() async { + await assertNoDiagnostics(r''' +var x = "has quote '"; +'''); + } + + test_doubleQuotes_innerSingleQuote_interpolation() async { + await assertNoDiagnostics(r''' +void f(String p) { + "has quote ' $p"; +} +'''); + } + + test_doubleQuotes_interpolation() async { + await assertDiagnostics(r''' +void f(String p) { + "no quote $p"; +} +''', [ + lint(21, 13), + ]); + } + + test_doubleQuotes_interpolationWithSingleQuote() async { + await assertNoDiagnostics(r''' +var x = "foo ${1 == 'x'} bar"; +'''); + } + + test_doubleQuotes_raw() async { + await assertDiagnostics(r''' +var x = r"no quote"; +''', [ + lint(8, 11), + ]); + } + + test_doubleQuotes_raw_innerSingleQuote() async { + await assertNoDiagnostics(r''' +var x = r"has quote '"; +'''); + } + + test_doubleQuotes_triple() async { + await assertDiagnostics(r''' +var x = r"""no quote"""; +''', [ + lint(8, 15), + ]); + } + + test_doubleQuotes_triple_innerSingleQuote() async { + await assertNoDiagnostics(r''' +var x = r"""has quote '"""; +'''); + } + + test_singleQuote() async { + await assertNoDiagnostics(r''' +var x = 'uses single'; +'''); + } + + test_singleQuote_import() async { + await assertNoDiagnostics(r''' +import 'dart:core'; +'''); + } + + test_singleQuote_interpolation() async { + await assertNoDiagnostics(r''' +void f(String p) { + 'uses single $p'; +} +'''); + } + + test_singleQuote_interpolationWithDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = 'foo ${1 == "x"} bar'; +'''); + } + + test_singleQuote_raw() async { + await assertNoDiagnostics(r''' +var x = r'uses single'; +'''); + } + + test_singleQuote_triple() async { + await assertNoDiagnostics(r""" +var x = r'''uses single'''; +"""); + } +} diff --git a/pkg/linter/test/rules/prefer_spread_collections_test.dart b/pkg/linter/test/rules/prefer_spread_collections_test.dart new file mode 100644 index 00000000000..c5c63b008df --- /dev/null +++ b/pkg/linter/test/rules/prefer_spread_collections_test.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferSpreadCollectionsTest); + }); +} + +@reflectiveTest +class PreferSpreadCollectionsTest extends LintRuleTest { + // TODO(srawlins): These tests mostly use a `dynamic` variable, but I don't + // think the lint rule is specific to `dynamic` values; it seems odd to + // specifically rely on them. + + @override + String get lintRule => 'prefer_spread_collections'; + + test_constInitializedWithNonConstantValue() async { + await assertDiagnostics(r''' +const thangs = []; +const cc = []..addAll(thangs); +''', [ + // No lint + error(CompileTimeErrorCode.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE, 30, + 18), + ]); + } + + test_listLiteralTarget_conditional() async { + await assertDiagnostics(r''' +dynamic x; +var y = ['a']..addAll(1 == 2 ? x : []); +''', [ + lint(26, 6), + ]); + } + + test_listLiteralTarget_conditional_constList() async { + await assertDiagnostics(r''' +dynamic x; +var y = ['a']..addAll(1 == 2 ? x : const []); +''', [ + lint(26, 6), + ]); + } + + test_listLiteralTarget_identifier() async { + await assertDiagnostics(r''' +dynamic x; +var y = []..addAll(x); +''', [ + lint(23, 6), + ]); + } + + test_listLiteralTarget_ifNull() async { + await assertDiagnostics(r''' +dynamic x; +var y = ['a']..addAll(x ?? []); +''', [ + lint(26, 6), + ]); + } + + test_listLiteralTarget_ifNull_constList() async { + await assertDiagnostics(r''' +dynamic x; +var y = ['a']..addAll(x ?? const []); +''', [ + lint(26, 6), + ]); + } + + test_listLiteralTarget_listLiteral() async { + // This is reported as `prefer_inlined_adds`. + await assertNoDiagnostics(r''' +var l = ['a']..addAll(['b']); +'''); + } + + test_listLiteralTarget_multipleCascades() async { + await assertDiagnostics(r''' +void f(List p) { + ['a']..addAll(p.map((i) => i.toString()))..addAll(['c']); +} +''', [ + lint(31, 6), + ]); + } + + test_nonCollection() async { + await assertNoDiagnostics(r''' +class A { + void addAll(Iterable iterable) {} +} + +void f() { + A()..addAll(['a']); +} +'''); + } + + test_otherListTarget_listLiteral() async { + await assertNoDiagnostics(r''' +var l1 = []; +var l2 = l1..addAll(['b']); +'''); + } +} diff --git a/pkg/linter/test/rules/prefer_typing_uninitialized_variables_test.dart b/pkg/linter/test/rules/prefer_typing_uninitialized_variables_test.dart new file mode 100644 index 00000000000..b8ac29a31b8 --- /dev/null +++ b/pkg/linter/test/rules/prefer_typing_uninitialized_variables_test.dart @@ -0,0 +1,121 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferTypingUninitializedVariablesTest); + }); +} + +@reflectiveTest +class PreferTypingUninitializedVariablesTest extends LintRuleTest { + @override + String get lintRule => 'prefer_typing_uninitialized_variables'; + + test_field_final_noInitializer() async { + await assertDiagnostics(r''' +class C { + final x; + C(this.x); +} +''', [ + lint(18, 1), + ]); + } + + test_field_typed() async { + await assertNoDiagnostics(r''' +class C { + String? x; +} +'''); + } + + test_field_var_noInitializer() async { + await assertDiagnostics(r''' +class C { + var x; +} +''', [ + lint(16, 1), + ]); + } + + test_field_var_noInitializer_notFirst() async { + await assertDiagnostics(r''' +class C { + var a = 5, + b; +} +''', [ + lint(29, 1), + ]); + } + + test_field_var_noInitializer_static() async { + await assertDiagnostics(r''' +class C { + static var x; +} +''', [ + lint(23, 1), + ]); + } + + test_forEachLoopVariable_final() async { + await assertNoDiagnostics(r''' +void f() { + for (final e in []) {} +} +'''); + } + + test_forLoopVariable_var_noInitializer() async { + await assertDiagnostics(r''' +void f() { + for (var i, j = 0; j < 5; i = j, j++) {} +} +''', [ + lint(22, 1), + ]); + } + + test_localVariable_var_initializer() async { + await assertNoDiagnostics(r''' +void f() { + // ignore: unused_local_variable + var x = 1; +} +'''); + } + + test_localVariable_var_noInitializer() async { + await assertDiagnostics(r''' +void f() { + // ignore: unused_local_variable + var x; +} +''', [ + lint(52, 1), + ]); + } + + test_topLevelVariable_var_initializer() async { + await assertNoDiagnostics(r''' +var x = 4; +'''); + } + + test_topLevelVariable_var_noInitializer() async { + await assertDiagnostics(r''' +var x; +''', [ + lint(4, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/prefer_void_to_null_test.dart b/pkg/linter/test/rules/prefer_void_to_null_test.dart new file mode 100644 index 00000000000..4baba91e1f0 --- /dev/null +++ b/pkg/linter/test/rules/prefer_void_to_null_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferVoidToNullTest); + }); +} + +@reflectiveTest +class PreferVoidToNullTest extends LintRuleTest { + @override + String get lintRule => 'prefer_void_to_null'; + + /// https://github.com/dart-lang/linter/issues/4201 + test_castAsExpression() async { + await assertNoDiagnostics(r''' +void f(int a) { + a as Null; +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4201 + test_castPattern() async { + await assertDiagnostics(r''' +void f(int a) { + switch (a) { + case var _ as Null: + } +} +''', [ + error(WarningCode.PATTERN_NEVER_MATCHES_VALUE_TYPE, 49, 4), + ]); + } + + test_localVariable() async { + await assertNoDiagnostics(r''' +void f() { + Null _; +} +'''); + } + + test_topLevelVariable() async { + await assertNoDiagnostics(r''' +Null a; +'''); + } +} diff --git a/pkg/linter/test/rules/provide_deprecation_message_test.dart b/pkg/linter/test/rules/provide_deprecation_message_test.dart new file mode 100644 index 00000000000..fad8189b00e --- /dev/null +++ b/pkg/linter/test/rules/provide_deprecation_message_test.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ProvideDeprecationMessageTest); + }); +} + +@reflectiveTest +class ProvideDeprecationMessageTest extends LintRuleTest { + @override + String get lintRule => 'provide_deprecation_message'; + + test_withoutMessage() async { + await assertDiagnostics(r''' +@deprecated +class C {} +''', [ + lint(0, 11), + ]); + } + + test_withMessage() async { + await assertNoDiagnostics(r''' +@Deprecated("Text.") +class C {} +'''); + } +} diff --git a/pkg/linter/test/rules/public_member_api_docs_test.dart b/pkg/linter/test/rules/public_member_api_docs_test.dart new file mode 100644 index 00000000000..3e2150ef5d8 --- /dev/null +++ b/pkg/linter/test/rules/public_member_api_docs_test.dart @@ -0,0 +1,336 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PublicMemberApiDocsTest); + defineReflectiveTests(PublicMemberApiDocsTestDirTest); + defineReflectiveTests(PublicMemberApiDocsExtensionTypesTest); + }); +} + +@reflectiveTest +class PublicMemberApiDocsExtensionTypesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'public_member_api_docs'; + + test_extensionTypeDeclaration() async { + await assertDiagnostics(r''' +extension type E(int i) { } +''', [ + lint(15, 1), + ]); + } + + test_field_instance() async { + await assertDiagnostics(r''' +/// Doc. +extension type E(int i) { + int? f; +} +''', [ + // No lint. + error(CompileTimeErrorCode.EXTENSION_TYPE_DECLARES_INSTANCE_FIELD, 46, 1), + ]); + } + + test_field_static() async { + await assertDiagnostics(r''' +/// Doc. +extension type E(int i) { + static int? f; +} +''', [ + lint(53, 1), + ]); + } + + test_method() async { + await assertDiagnostics(r''' +/// Doc. +extension type E(int i) { + void m() { } +} +''', [ + lint(46, 1), + ]); + } + + test_method_private() async { + await assertDiagnostics(r''' +/// Doc. +extension type E(int i) { + void _m() { } +} +''', [ + // No lint + error(WarningCode.UNUSED_ELEMENT, 46, 2), + ]); + } +} + +@reflectiveTest +class PublicMemberApiDocsTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'public_member_api_docs'; + + /// https://github.com/dart-lang/linter/issues/4526 + test_abstractFinalConstructor() async { + await assertDiagnostics(r''' +abstract final class S { + S(); +} + +final class A extends S {} +''', [ + lint(21, 1), + // No lint on `S()` declaration + lint(47, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4526 + test_abstractInterfaceConstructor() async { + await assertDiagnostics(r''' +abstract interface class S { + S(); +} + +final class A extends S {} +''', [ + lint(25, 1), + // No lint on `S()` declaration + lint(51, 1), + ]); + } + + test_annotatedEnumValue() async { + await assertNoDiagnostics(r''' +/// Documented. +enum A { + /// This represents 'a'. + @Deprecated("Use 'b'") + a, + + /// This represents 'b'. + b; +} +'''); + } + + test_enum() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + int x() => 0; + int get y => 1; +} +''', [ + lint(5, 1), + lint(11, 1), + lint(13, 1), + lint(15, 1), + lint(24, 1), + lint(44, 1), + ]); + } + + test_enum_privateConstant() async { + await assertDiagnostics(r''' +/// Documented. +enum A { + _a; +} +''', [ + // No lint. + error(WarningCode.UNUSED_FIELD, 27, 2), + ]); + } + + test_enumConstructor() async { + await assertNoDiagnostics(r''' +/// Documented. +enum A { + /// This represents 'a'. + a(), + + /// This represents 'b'. + b(); + + const A(); +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3525 + test_extension() async { + await assertDiagnostics(r''' +extension E on Object { + void f() { } +} +''', [ + lint(10, 1), + lint(31, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4521 + test_internalClass() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@internal +class C { + int? f; + int get i => 0; + C(); + void m() {} +} +''', [ + // Technically not in the private API but we can ignore that for testing. + error(WarningCode.INVALID_INTERNAL_ANNOTATION, 34, 9), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4521 + test_internalEnum() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@internal +enum E { + a, b, c; + int get i => 0; + void m() {} + + const E(); +} +''', [ + // Technically not in the private API but we can ignore that for testing. + error(WarningCode.INVALID_INTERNAL_ANNOTATION, 34, 9), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4521 + test_internalExtension() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@internal +extension X on Object { + static int? f; + static int get i => 0; + void e() {} +} +''', [ + // Technically not in the private API but we can ignore that for testing. + error(WarningCode.INVALID_INTERNAL_ANNOTATION, 34, 9), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4521 + test_internalMixin() async { + await assertDiagnostics(r''' +import 'package:meta/meta.dart'; + +@internal +mixin M { + int? f; + int get i => 0; + void m() {} +} +''', [ + // Technically not in the private API but we can ignore that for testing. + error(WarningCode.INVALID_INTERNAL_ANNOTATION, 34, 9), + ]); + } + + test_mixin_method() async { + await assertDiagnostics(r''' +/// A mixin M. +mixin M { + String m() => ''; +}''', [ + lint(34, 1), + ]); + } + + test_mixin_overridingMethod_OK() async { + await assertNoDiagnostics(r''' +/// A mixin M. +mixin M { + @override + String toString() => ''; +}'''); + } + + /// https://github.com/dart-lang/linter/issues/4526 + test_sealedConstructor() async { + await assertDiagnostics(r''' +sealed class S { + S(); +} + +final class A extends S {} +''', [ + lint(13, 1), + // No lint on `S()` declaration + lint(39, 1), + ]); + } + + test_topLevelMembers() async { + await assertDiagnostics(r''' +int g = 1; +typedef T = void Function(); +int get z => 0; +''', [ + lint(4, 1), + lint(19, 1), + lint(48, 1), + ]); + } + + test_topLevelMembers_private() async { + await assertDiagnostics(r''' +int _h = 1; +typedef _T = void Function(); +int get _z => 0; +''', [ + // No lint + error(WarningCode.UNUSED_ELEMENT, 4, 2), + error(WarningCode.UNUSED_ELEMENT, 20, 2), + error(WarningCode.UNUSED_ELEMENT, 50, 2), + ]); + } +} + +@reflectiveTest +class PublicMemberApiDocsTestDirTest extends LintRuleTest { + @override + String get lintRule => 'public_member_api_docs'; + + @override + String get testPackageLibPath => '$testPackageRootPath/test'; + + test_inTestDir() async { + await assertNoDiagnostics(r''' +String? b; +typedef T = void Function(); +'''); + } +} diff --git a/pkg/linter/test/rules/recursive_getters_test.dart b/pkg/linter/test/rules/recursive_getters_test.dart new file mode 100644 index 00000000000..e2cc12369ed --- /dev/null +++ b/pkg/linter/test/rules/recursive_getters_test.dart @@ -0,0 +1,260 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(RecursiveGettersTest); + }); +} + +@reflectiveTest +class RecursiveGettersTest extends LintRuleTest { + @override + String get lintRule => 'recursive_getters'; + + /// https://github.com/dart-lang/linter/issues/3706 + test_constList_expressionFunctionBody() async { + var listContents = StringBuffer(); + for (var i = 0; i < 100000; ++i) { + listContents.write('"foo", '); + } + await assertNoDiagnostics(''' +List get strings => const [$listContents]; +'''); + } + + test_constMap_expressionFunctionBody() async { + var mapContents = StringBuffer(); + for (var i = 0; i < 100000; ++i) { + mapContents.write('$i : "foo", '); + } + await assertNoDiagnostics(''' +Map get strings => const {$mapContents}; +'''); + } + + /// https://github.com/dart-lang/linter/issues/3706 + test_constSet_expressionFunctionBody() async { + var setContents = StringBuffer(); + for (var i = 0; i < 100000; ++i) { + setContents.write('"foo$i", '); + } + await assertNoDiagnostics(''' +Set get strings => const {$setContents}; +'''); + } + + test_instanceGetter_blockBody_nonRecursive() async { + await assertNoDiagnostics(r''' +class C { + int _f = 0; + int get f { + return _f; + } +} +'''); + } + + test_instanceGetter_blockBody_recursiveWithImplicitThis() async { + await assertDiagnostics(r''' +class C { + int get f { + return f; + } +} +''', [ + lint(35, 1), + ]); + } + + test_instanceGetter_expressionBody_innerRecursiveCall() async { + await assertDiagnostics(r''' +class C { + int get f => p(f); +} +int p(int arg) => 0; +''', [ + lint(27, 1), + ]); + } + + test_instanceGetter_expressionBody_nonRecursive() async { + await assertNoDiagnostics(r''' +class C { + int _f = 0; + int get f => _f; +} +'''); + } + + test_instanceGetter_expressionBody_recursiveWithExplicitThis() async { + await assertDiagnostics(r''' +class C { + int get f => this.f; +} +''', [ + lint(30, 1), + ]); + } + + test_instanceGetter_expressionBody_recursiveWithImplicitThis() async { + await assertDiagnostics(r''' +class C { + int get f => f; +} +''', [ + lint(25, 1), + ]); + } + + test_instanceGetter_recursiveCallOnOtherInstance() async { + await assertNoDiagnostics(r''' +class C { + C? _parent; + C? get ancestor => _parent?.ancestor; +} +'''); + } + + test_method_recursive() async { + await assertNoDiagnostics(r''' +class C { + int m() => m(); +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/586 + test_nestedReference() async { + await assertNoDiagnostics(r''' +class Nested { + final Nested _parent; + Nested(this._parent); + Nested get ancestor => _parent.ancestor; +} +'''); + } + + test_nestedReference_property() async { + await assertNoDiagnostics(r''' +class Nested { + final Nested target; + Nested(this.target); + bool get isFoo { + var self = this; + return self.target.isFoo; + } +} +'''); + } + + test_referenceInListLiteral() async { + await assertDiagnostics(r''' +class C { + List get f => [1, 2, ...f]; +} +''', [ + lint(41, 1), + ]); + } + + test_referenceInMapLiteral() async { + await assertDiagnostics(r''' +class C { + Map get f => {}..addAll(f); +} +''', [ + lint(46, 1), + ]); + } + + test_referenceInMethodCall() async { + await assertDiagnostics(r''' +class C { + int get f { + print(f); + return 0; + } +} +''', [ + lint(34, 1), + ]); + } + + test_simpleGetter() async { + await assertDiagnostics(r''' +class C { + int get f => f; +} +''', [ + lint(25, 1), + ]); + } + + test_simpleGetter_thisPrefix() async { + await assertDiagnostics(r''' +class C { + int get f => this.f; +} +''', [ + lint(30, 1), + ]); + } + + test_staticMemberReference() async { + await assertNoDiagnostics(r''' +class C { + static final c = C(); + int get f => c.f; +} +'''); + } + + test_topLevelGetter_blockBody_nonRecursive() async { + await assertNoDiagnostics(r''' +int _f = 1; +int get f { + return _f; +} +'''); + } + + test_topLevelGetter_blockBody_recursiveCall() async { + await assertDiagnostics(r''' +int get f { + return f; +} +''', [ + lint(21, 1), + ]); + } + + test_topLevelGetter_expressionBody_nonRecursive() async { + await assertNoDiagnostics(r''' +int _f = 1; +int get f => _f; +'''); + } + + test_topLevelGetter_expressionBody_recursiveCall() async { + await assertDiagnostics(r''' +int get f => f; +''', [ + lint(13, 1), + ]); + } + + test_topLevelGetter_innerRecursiveCall() async { + await assertDiagnostics(r''' +int? _f = 1; +int get f => _f == null ? 0 : f; +''', [ + lint(43, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/secure_pubspec_urls_test.dart b/pkg/linter/test/rules/secure_pubspec_urls_test.dart new file mode 100644 index 00000000000..f398f154f3c --- /dev/null +++ b/pkg/linter/test/rules/secure_pubspec_urls_test.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SecurePubspecUrlsTest); + }); +} + +@reflectiveTest +class SecurePubspecUrlsTest extends LintRuleTest { + @override + bool get dumpAstOnFailures => false; + + @override + String get lintRule => 'secure_pubspec_urls'; + + test_dependencyGit_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dependencies: + kittens: + git: http://github.com/munificent/kittens.git +''', [ + lint(81, 40), + ]); + } + + test_dependencyGitUrl_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dependencies: + kittens2: + git: + url: http://github.com/munificent/kittens2.git + ref: main +''', [ + lint(93, 41), + ]); + } + + test_dependencyHosted_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dependencies: + transmogrify: + hosted: http://some-package-server.com +''', [ + lint(89, 30), + ]); + } + + test_dependencyHostedUrl_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dependencies: + transmogrify: + hosted: + name: transmogrify + url: http://some-package-server.com + version: ^1.0.0 +''', [ + lint(125, 30), + ]); + } + + test_dependencyOverridesGit_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dependency_overrides: + kittens: + git: http://github.com/munificent/kittens.git +''', [ + lint(89, 40), + ]); + } + + test_devDependencyGit_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 + +dev_dependencies: + kittens: + git: http://github.com/munificent/kittens.git +''', [ + lint(85, 40), + ]); + } + + test_homepage_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 +homepage: http://github.com/dart-lang/linter +''', [ + lint(56, 34), + ]); + } + + test_homepage_secure() async { + await assertNoPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 +homepage: https://github.com/dart-lang/linter +'''); + } + + test_issueTracker_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 +issue_tracker: http://github.com/dart-lang/linter/issues +''', [ + lint(61, 41), + ]); + } + + test_repository_insecure() async { + await assertPubspecDiagnostics(r''' +name: fancy +description: Text. +version: 1.1.1 +repository: http://github.com/dart-lang/linter + +environment: + sdk: ">=2.15.2 <3.0.0" +''', [ + lint(58, 34), + ]); + } +} diff --git a/pkg/linter/test/rules/sized_box_for_whitespace_test.dart b/pkg/linter/test/rules/sized_box_for_whitespace_test.dart new file mode 100644 index 00000000000..8fb133a3aa3 --- /dev/null +++ b/pkg/linter/test/rules/sized_box_for_whitespace_test.dart @@ -0,0 +1,145 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SizedBoxForWhitespaceTest); + }); +} + +@reflectiveTest +class SizedBoxForWhitespaceTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'sized_box_for_whitespace'; + + test_hasChild() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + child: Row(), + ); +} +'''); + } + + test_hasHeight_andChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + height: 10, + child: Row(), + ); +} +''', [ + lint(62, 9), + ]); + } + + test_hasHeight_noChild() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + height:10, + ); +} +'''); + } + + test_hasWidth_andChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + width: 10, + child: Row(), + ); +} +''', [ + lint(62, 9), + ]); + } + + test_hasWidth_noChild() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + width: 10, + ); +} +'''); + } + + test_hasWidthAndHeight_andChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + width: 10, + height: 10, + child: Row(), + ); +} +''', [ + lint(62, 9), + ]); + } + + test_hasWidthAndHeight_andKey_noChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + key: Key(''), + width: 10, + height: 10, + ); +} +''', [ + lint(62, 9), + ]); + } + + test_hasWidthAndHeight_noChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + width: 10, + height: 10, + ); +} +''', [ + lint(62, 9), + ]); + } + + test_noArguments() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget emptyContainer() { + return Container(); +} +'''); + } +} diff --git a/pkg/linter/test/rules/slash_for_doc_comments_test.dart b/pkg/linter/test/rules/slash_for_doc_comments_test.dart new file mode 100644 index 00000000000..237e7b5c312 --- /dev/null +++ b/pkg/linter/test/rules/slash_for_doc_comments_test.dart @@ -0,0 +1,207 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SlashForDocCommentsTest); + }); +} + +@reflectiveTest +class SlashForDocCommentsTest extends LintRuleTest { + @override + String get lintRule => 'slash_for_doc_comments'; + + test_class() async { + await assertDiagnostics(r''' +/** C */ +class C {} +''', [ + lint(0, 8), + ]); + } + + test_constructor() async { + await assertDiagnostics(r''' +class C { + /** C */ + C(); +} +''', [ + lint(12, 8), + ]); + } + + test_enum() async { + await assertDiagnostics(r''' +/** E */ +enum E { + A, + B +} +''', [ + lint(0, 8), + ]); + } + + test_enumConstant() async { + await assertDiagnostics(r''' +enum E { + /** A */ + A, + B +} +''', [ + lint(11, 8), + ]); + } + + test_extension() async { + await assertDiagnostics(r''' +/** A */ +extension on int {} +''', [ + lint(0, 8), + ]); + } + + test_field() async { + await assertDiagnostics(r''' +class C { + /** x */ + var x; +} +''', [ + lint(12, 8), + ]); + } + + test_library() async { + await assertDiagnostics(r''' +/** l */ +library l; +''', [ + lint(0, 8), + ]); + } + + test_localFunction() async { + await assertDiagnostics(r''' +void f() { + /** g */ + // ignore: unused_element + void g() {} +} +''', [ + lint(13, 8), + ]); + } + + test_method() async { + await assertDiagnostics(r''' +class C { + /** f */ + void f() {} +} +''', [ + lint(12, 8), + ]); + } + + test_mixin() async { + await assertDiagnostics(r''' +/** M */ +mixin M {} +''', [ + lint(0, 8), + ]); + } + + test_mixinApplication() async { + await assertDiagnostics(r''' +/** C */ +class C = Object with M; + +mixin M {} +''', [ + lint(0, 8), + ]); + } + + test_noComment() async { + await assertNoDiagnostics(r''' +mixin M {} +'''); + } + + test_oneAsterisk_class() async { + await assertNoDiagnostics(r''' +/* C */ +class C {} +'''); + } + + test_oneAsterisk_mixin() async { + await assertNoDiagnostics(r''' +/* M */ +mixin M {} +'''); + } + + test_threeSlashes_class() async { + await assertNoDiagnostics(r''' +/// OK +class C {} +'''); + } + + test_topLevelFunction() async { + await assertDiagnostics(r''' +/** f */ +void f() {} +''', [ + lint(0, 8), + ]); + } + + test_topLevelVariable() async { + await assertDiagnostics(r''' +/** x */ +var x = 1; +''', [ + lint(0, 8), + ]); + } + + test_typedef_genericFunctionType() async { + await assertDiagnostics(r''' +/** T */ +typedef T = bool Function(); +''', [ + lint(0, 8), + ]); + } + + test_typedef_legacy() async { + await assertDiagnostics(r''' +/** F */ +typedef bool F(); +''', [ + lint(0, 8), + ]); + } + + test_typedef_nonFunction() async { + await assertDiagnostics(r''' +/** T */ +typedef T = Object; +''', [ + lint(0, 8), + ]); + } +} diff --git a/pkg/linter/test/rules/sort_constructors_first_test.dart b/pkg/linter/test/rules/sort_constructors_first_test.dart new file mode 100644 index 00000000000..633a73f2674 --- /dev/null +++ b/pkg/linter/test/rules/sort_constructors_first_test.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SortConstructorsFirstTest); + }); +} + +@reflectiveTest +class SortConstructorsFirstTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'sort_constructors_first'; + + test_constructorBeforeMethod() async { + await assertNoDiagnostics(r''' +abstract class A { + const A(); + void f(); +} +'''); + } + + test_fieldBeforeConstructor() async { + await assertDiagnostics(r''' +abstract class A { + final a = 0; + A(); +} +''', [ + lint(36, 1), + ]); + } + + test_methodBeforeConstructor() async { + await assertDiagnostics(r''' +abstract class A { + void f(); + const A(); +} +''', [ + lint(39, 1), + ]); + } + + test_methodBeforeConstructor_extensionType() async { + // Since the check logic is shared w/ classes and enums, one test should + // provide sufficient coverage for extension types. + await assertDiagnostics(r''' +extension type E(Object o) { + void f() {} + E.e(this.o); +} +''', [ + lint(45, 1), + ]); + } + + test_methodBeforeConstructors() async { + await assertDiagnostics(r''' +abstract class A { + void f(); + A(); + A.named(); +} +''', [ + lint(33, 1), + lint(40, 1), + ]); + } + + test_ok() async { + await assertNoDiagnostics(r''' +enum A { + a,b,c; + const A(); + int f() => 0; +} +'''); + } + + test_staticFieldBeforeConstructor() async { + await assertDiagnostics(r''' +abstract class A { + static final a = 0; + A(); +} +''', [ + lint(43, 1), + ]); + } + + test_unsorted() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + int f() => 0; + const A(); +} +''', [ + lint(42, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/sort_pub_dependencies_test.dart b/pkg/linter/test/rules/sort_pub_dependencies_test.dart new file mode 100644 index 00000000000..fef64623d77 --- /dev/null +++ b/pkg/linter/test/rules/sort_pub_dependencies_test.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SortPubDependenciesTest); + }); +} + +@reflectiveTest +class SortPubDependenciesTest extends LintRuleTest { + @override + bool get dumpAstOnFailures => false; + + @override + String get lintRule => 'sort_pub_dependencies'; + + test_dependencies_sorted() async { + await assertNoPubspecDiagnostics(r''' +name: fancy +version: 1.1.1 + +dependencies: + aaa: ^0.1.1 + bbb: ^0.15.8 + flutter: + sdk: flutter +'''); + } + + test_dependencies_duplicates() async { + await assertNoPubspecDiagnostics(r''' +name: fancy +version: 1.1.1 + +dependencies: + aaa: ^0.1.1 + aaa: ^0.1.1 + bbb: ^0.15.8 + flutter: + sdk: flutter +'''); + } + + test_dependencies_unsorted() async { + await assertPubspecDiagnostics(r''' +name: fancy +version: 1.1.1 + +dependencies: + aaa: ^0.1.1 + flutter: + sdk: flutter + bbb: ^0.15.8 +''', [ + lint(86, 3), + ]); + } + + test_dependencyOverrides_unsorted() async { + await assertPubspecDiagnostics(r''' +name: fancy +version: 1.1.1 + +dependency_overrides: + aaa: ^0.1.1 + flutter: + sdk: flutter + bbb: ^0.15.8 +''', [ + lint(94, 3), + ]); + } + + test_devDependencies_unsorted() async { + await assertPubspecDiagnostics(r''' +name: fancy +version: 1.1.1 + +dev_dependencies: + aaa: ^0.1.1 + flutter: + sdk: flutter + bbb: ^0.15.8 +''', [ + lint(90, 3), + ]); + } +} diff --git a/pkg/linter/test/rules/sort_unnamed_constructors_first_test.dart b/pkg/linter/test/rules/sort_unnamed_constructors_first_test.dart new file mode 100644 index 00000000000..897e4fc2a60 --- /dev/null +++ b/pkg/linter/test/rules/sort_unnamed_constructors_first_test.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(SortUnnamedConstructorsFirstTest); + }); +} + +@reflectiveTest +class SortUnnamedConstructorsFirstTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'sort_unnamed_constructors_first'; + + test_class_sorted() async { + await assertNoDiagnostics(r''' +class C { + C(); + C.named(); + // ignore: unused_element + C._(); +} +'''); + } + + test_class_unsorted() async { + await assertDiagnostics(r''' +class C { + C.named(); + C(); + // ignore: unused_element + C._(); +} +''', [ + lint(25, 1), + ]); + } + + test_enum_sorted() async { + await assertNoDiagnostics(r''' +enum A { + a,b,c.aa(); + const A(); + const A.aa(); +} +'''); + } + + test_enum_unsorted() async { + await assertDiagnostics(r''' +enum A { + a,b,c.aa(); + const A.aa(); + const A(); +} +''', [ + lint(47, 1), + ]); + } + + test_extensionType() async { + await assertDiagnostics(r''' +extension type E.a(Object o) { + void m() { } + E.b(this.o); + E(this.o); +} +''', [ + lint(63, 1), + ]); + } + + test_extensionType_invalidConstructor() async { + await assertDiagnostics(r''' +extension type E(Object o) { + void m() { } + E(this.o); +} +''', [ + // No lint. + error(CompileTimeErrorCode.DUPLICATE_CONSTRUCTOR_DEFAULT, 46, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/test_types_in_equals_test.dart b/pkg/linter/test/rules/test_types_in_equals_test.dart new file mode 100644 index 00000000000..72c86255185 --- /dev/null +++ b/pkg/linter/test/rules/test_types_in_equals_test.dart @@ -0,0 +1,51 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(TestTypesInEqualsTest); + }); +} + +@reflectiveTest +class TestTypesInEqualsTest extends LintRuleTest { + @override + String get lintRule => 'test_types_in_equals'; + + test_usesIs() async { + await assertNoDiagnostics(r''' +class C { + int? x; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is C && this.x == other.x; + } +} +'''); + } + + test_doesNotUseIs() async { + await assertDiagnostics(r''' +class C { + int? x; + + @override + bool operator ==(Object other) { + C otherC = other as C; + return otherC.x == x; + } +} +''', [ + lint(83, 10), + ]); + } +} diff --git a/pkg/linter/test/rules/throw_in_finally_test.dart b/pkg/linter/test/rules/throw_in_finally_test.dart new file mode 100644 index 00000000000..60b4ef3d042 --- /dev/null +++ b/pkg/linter/test/rules/throw_in_finally_test.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ThrowInFinallyTest); + }); +} + +@reflectiveTest +class ThrowInFinallyTest extends LintRuleTest { + @override + String get lintRule => 'throw_in_finally'; + + test_noThrow() async { + await assertNoDiagnostics(r''' +void f() { + try { + } catch (e) { + } finally { + print(''); + } +} +'''); + } + + test_throwInCatchInFinally() async { + await assertNoDiagnostics(r''' +void f() { + try { + } catch (e) { + } finally { + try { + } catch (e) { + throw C(); + } + } +} + +class C {} +'''); + } + + test_throwInFinally() async { + await assertDiagnostics(r''' + void f() { + try { + } catch (e) { + } finally { + if (1 > 0) { + throw ''; + } else { + print('should catch nested throws!'); + } + } +} +''', [ + lint(74, 8), + ]); + } + + test_throwInInnerClosureInFinally() async { + await assertNoDiagnostics(r''' +void f() { + try { + registrationGuard(); + } finally { + registrationGuard = () { + throw C(); + }; + } +} +Function registrationGuard = () {}; + +class C {} +'''); + } +} diff --git a/pkg/linter/test/rules/tighten_type_of_initializing_formals_test.dart b/pkg/linter/test/rules/tighten_type_of_initializing_formals_test.dart new file mode 100644 index 00000000000..38af169eb3b --- /dev/null +++ b/pkg/linter/test/rules/tighten_type_of_initializing_formals_test.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(TightenTypeOfInitializingFormalsTest); + }); +} + +@reflectiveTest +class TightenTypeOfInitializingFormalsTest extends LintRuleTest { + @override + String get lintRule => 'tighten_type_of_initializing_formals'; + + test_superInit() async { + await assertDiagnostics(r''' +class A { + String? a; + A(this.a); +} + +class B extends A { + B(String super.a); +} + +class C extends A { + C(super.a) : assert(a != null); +} +''', [ + lint(107, 7), + ]); + } +} diff --git a/pkg/linter/test/rules/type_annotate_public_apis_test.dart b/pkg/linter/test/rules/type_annotate_public_apis_test.dart new file mode 100644 index 00000000000..4f9e913ee87 --- /dev/null +++ b/pkg/linter/test/rules/type_annotate_public_apis_test.dart @@ -0,0 +1,342 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + // TODO(srawlins): Add tests with constructor parameters, enums, unnamed + // extensions. + defineReflectiveSuite(() { + defineReflectiveTests(TypeAnnotatePublicApisTest); + }); +} + +@reflectiveTest +class TypeAnnotatePublicApisTest extends LintRuleTest { + @override + String get lintRule => 'type_annotate_public_apis'; + + test_instanceField_onClass_hasInitializer() async { + await assertNoDiagnostics(r''' +class A { + final x = 0; +} +'''); + } + + test_instanceField_onClass_hasVar_noInitializer() async { + await assertDiagnostics(r''' +class A { + var x; +} +''', [ + lint(16, 1), + ]); + } + + test_instanceField_onClass_inDeclarationList() async { + await assertDiagnostics(r''' +class A { + // ignore: unused_field + var x, _y; +} +''', [ + lint(42, 1), + ]); + } + + test_instanceField_onClass_noInitializer() async { + await assertNoDiagnostics(r''' +class A { + final x = 0; +} +'''); + } + + test_instanceField_onClass_nullInitializer() async { + await assertDiagnostics(r''' +class A { + final n = null; +} +''', [ + lint(18, 1), + ]); + } + + test_instanceGetter_onClass() async { + await assertNoDiagnostics(r''' +class A { + int get x => 42; +} +'''); + } + + test_instanceGetter_onClass_noReturnType() async { + await assertDiagnostics(r''' +class A { + get x => 42; +} +''', [ + lint(16, 1), + ]); + } + + test_instanceGetter_onExtension_noReturnType() async { + await assertDiagnostics(r''' +extension E on int { + get x => 0; +} +''', [ + lint(27, 1), + ]); + } + + test_instanceMethod_onClass_noReturnType() async { + await assertDiagnostics(r''' +class A { + m() {} +} +''', [ + lint(12, 1), + ]); + } + + test_instanceMethod_onClass_parameterMissingType() async { + await assertDiagnostics(r''' +class A { + void m(x) {} +} +''', [ + lint(19, 1), + ]); + } + + test_instanceMethod_onExtension_noReturnType() async { + await assertDiagnostics(r''' +extension E on int { + f() {} +} +''', [ + lint(23, 1), + ]); + } + + test_instanceMethod_onExtension_parameterMissingType() async { + await assertDiagnostics(r''' +extension E on int { + void m(p) {} +} +''', [ + lint(30, 1), + ]); + } + + test_instanceMethod_parameterNameIsMultipleUnderscores() async { + await assertNoDiagnostics(r''' +class A { + void m(__) {} +} +'''); + } + + test_instanceMethod_parameterNameIsUnderscore() async { + await assertNoDiagnostics(r''' +class A { + void m(_) {} +} +'''); + } + + test_instanceSetter_noReturnType() async { + await assertNoDiagnostics(r''' +class A { + set x(int p) {} +} +'''); + } + + test_instanceSetter_onClass_parameterMissingType() async { + await assertDiagnostics(r''' +class A { + set x(p) {} +} +''', [ + lint(18, 1), + ]); + } + + test_instanceSetter_parameterMissingType() async { + await assertDiagnostics(r''' +extension E on int { + set x(p) {} +} +''', [ + lint(29, 1), + ]); + } + + test_instanceSetter_private_parameterMissingType() async { + await assertNoDiagnostics(r''' +extension E on int { + // ignore: unused_element + set _x(p) {} +} +'''); + } + + test_localFunction() async { + await assertNoDiagnostics(r''' +void f() { + // ignore: unused_element + void g(x) {} +} +'''); + } + + test_staticConstField_hasInitializer() async { + await assertNoDiagnostics(r''' +class A { + static const x = ''; +} +'''); + } + + test_staticField_hasInitializer() async { + await assertNoDiagnostics(r''' +class A { + static final x = 3; +} +'''); + } + + test_staticField_noInitializer() async { + await assertNoDiagnostics(r''' +class A { + static final x = 0; +} +'''); + } + + test_staticField_nullInitializer() async { + await assertDiagnostics(r''' +class A { + static final x = null; +} +''', [ + lint(25, 1), + ]); + } + + test_staticField_withInitializer() async { + await assertNoDiagnostics(r''' +class A { + static final x = 0; +} +'''); + } + + test_staticMethod_onClass_noReturnType() async { + await assertDiagnostics(r''' +class A { + static m() {} +} +''', [ + lint(19, 1), + ]); + } + + test_staticMethod_onClass_parameterHasVar() async { + await assertDiagnostics(r''' +class A { + static void m(var p) {} +} +''', [ + lint(26, 5), + ]); + } + + test_staticMethod_onClass_parameterMissingType() async { + await assertDiagnostics(r''' +class A { + static void m(p) {} +} +''', [ + lint(26, 1), + ]); + } + + test_topLevelConst() async { + await assertNoDiagnostics(r''' +const x = ''; +'''); + } + + test_topLevelFunction_noReturnType() async { + await assertDiagnostics(r''' +f() {} +''', [ + lint(0, 1), + ]); + } + + test_topLevelFunction_parameterMissingType() async { + await assertDiagnostics(r''' +void f(x) {} +''', [ + lint(7, 1), + ]); + } + + test_topLevelGetter_hasReturnType() async { + await assertNoDiagnostics(r''' +int get x => 42; +'''); + } + + test_topLevelGetter_noReturnType() async { + await assertDiagnostics(r''' +get x => 42; +''', [ + lint(4, 1), + ]); + } + + test_topLevelSetter_parameterHasType() async { + await assertNoDiagnostics(r''' +set x(int p) {} +'''); + } + + test_topLevelSetter_parameterMissingType() async { + await assertDiagnostics(r''' +set x(p) {} +''', [ + lint(6, 1), + ]); + } + + test_typedefLegacy_parameterMissingType() async { + await assertDiagnostics(r''' +typedef F(x); +''', [ + lint(8, 1), + ]); + } + + test_typedefLegacy_private_parameterHasType() async { + await assertNoDiagnostics(r''' +// ignore: unused_element +typedef _F(int value); +'''); + } + + test_typedefLegacy_private_parameterMissingType() async { + await assertNoDiagnostics(r''' +// ignore: unused_element +typedef void _F(value); +'''); + } +} diff --git a/pkg/linter/test/rules/type_init_formals_test.dart b/pkg/linter/test/rules/type_init_formals_test.dart new file mode 100644 index 00000000000..276ff0eccd2 --- /dev/null +++ b/pkg/linter/test/rules/type_init_formals_test.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(TypeInitFormalsTest); + defineReflectiveTests(TypeInitFormalsSuperTest); + }); +} + +@reflectiveTest +class TypeInitFormalsSuperTest extends LintRuleTest { + @override + String get lintRule => 'type_init_formals'; + + test_super() async { + await assertDiagnostics(r''' +class A { + String? a; + A({this.a}); +} + +class B extends A { + B({String? super.a}); +} +''', [ + lint(66, 7), + ]); + } +} + +@reflectiveTest +class TypeInitFormalsTest extends LintRuleTest { + @override + String get lintRule => 'type_init_formals'; + + test_extraPositionalArgument() async { + await assertDiagnostics(r''' +class A { + String? p1; + String p2 = ''; + A.y({required String? this.p2}); +} +''', [ + // No lint + error(CompileTimeErrorCode.FIELD_INITIALIZING_FORMAL_NOT_ASSIGNABLE, 49, + 24), + ]); + } + + test_initializingFormalForNonExistentField() async { + await assertDiagnostics(r''' +class Invalid { + Invalid(int this.x); // OK +} +''', [ + // No lint + error(CompileTimeErrorCode.INITIALIZING_FORMAL_FOR_NON_EXISTENT_FIELD, 26, + 10), + ]); + } +} diff --git a/pkg/linter/test/rules/type_literal_in_constant_pattern_test.dart b/pkg/linter/test/rules/type_literal_in_constant_pattern_test.dart new file mode 100644 index 00000000000..849e2c8ee13 --- /dev/null +++ b/pkg/linter/test/rules/type_literal_in_constant_pattern_test.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/rules/type_literal_in_constant_pattern.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferPatternTypeLiteralEqualityTest); + }); +} + +@reflectiveTest +class PreferPatternTypeLiteralEqualityTest extends LintRuleTest { + @override + String get lintRule => TypeLiteralInConstantPattern.lintName; + + test_constNotType_matchObjectNullable() async { + await assertNoDiagnostics(r''' +void f(Object? x) { + if (x case 0) {} +} +'''); + } + + test_constNotType_matchType() async { + await assertDiagnostics(r''' +void f(Type x) { + if (x case 0) {} +} +''', [ + error(WarningCode.CONSTANT_PATTERN_NEVER_MATCHES_VALUE_TYPE, 30, 1), + ]); + } + + test_constType_matchDynamic() async { + await assertDiagnostics(r''' +void f(dynamic x) { + if (x case int) {} +} +''', [ + lint(33, 3), + ]); + } + + test_constType_matchObject() async { + await assertDiagnostics(r''' +void f(Object x) { + if (x case int) {} +} +''', [ + lint(32, 3), + ]); + } + + test_constType_matchObjectNullable() async { + await assertDiagnostics(r''' +void f(Object? x) { + if (x case int) {} +} +''', [ + lint(33, 3), + ]); + } + + test_constType_matchType() async { + await assertDiagnostics(r''' +void f(Type x) { + if (x case int) {} +} +''', [ + lint(30, 3), + ]); + } + + test_constType_matchType_explicitConst() async { + await assertNoDiagnostics(r''' +void f(Type x) { + if (x case const (int)) {} +} +'''); + } + + test_constType_matchType_nested() async { + await assertDiagnostics(r''' +void f(A x) { + if (x case A(type: int)) {} +} + +class A { + final Type type; + A(this.type); +} +''', [ + lint(35, 3), + ]); + } + + test_constType_matchTypeParameter_boundObjectNullable() async { + await assertDiagnostics(r''' +void f(T x) { + if (x case int) {} +} +''', [ + lint(46, 3), + ]); + } + + /// Nobody will write such code, but just in case. + test_constType_matchTypeParameter_boundType() async { + await assertDiagnostics(r''' +void f(T x) { + if (x case int) {} +} +''', [ + lint(43, 3), + ]); + } +} diff --git a/pkg/linter/test/rules/unawaited_futures_test.dart b/pkg/linter/test/rules/unawaited_futures_test.dart new file mode 100644 index 00000000000..8a3a84bedc9 --- /dev/null +++ b/pkg/linter/test/rules/unawaited_futures_test.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnawaitedFuturesTest); + }); +} + +@reflectiveTest +class UnawaitedFuturesTest extends LintRuleTest { + @override + String get lintRule => 'unawaited_futures'; + + test_undefinedIdentifier() async { + await assertDiagnostics(r''' +f() async { + Duration d = Duration(); + Future.delayed(d, bar); +} +''', [ + // No lint + error(CompileTimeErrorCode.UNDEFINED_IDENTIFIER, 59, 3), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_await_in_return_test.dart b/pkg/linter/test/rules/unnecessary_await_in_return_test.dart new file mode 100644 index 00000000000..2892600a3c5 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_await_in_return_test.dart @@ -0,0 +1,168 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryAwaitInReturnTest); + }); +} + +@reflectiveTest +class UnnecessaryAwaitInReturnTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_await_in_return'; + + test_asyncArrow_awaited() async { + await assertDiagnostics(r''' +Future f() async => await future; +final future = Future.value(1); +''', [ + lint(25, 5), + ]); + } + + test_asyncArrow_awaited_futureOfFuture() async { + await assertNoDiagnostics(r''' +Future f() async => await future; +final future = Future>.value(Future.value(1)); +'''); + } + + test_asyncArrow_awaited_instanceMethod() async { + await assertDiagnostics(r''' +class A { + Future f() async => await future; +} +final future = Future.value(1); +''', [ + lint(37, 5), + ]); + } + + test_asyncArrow_awaited_subtype() async { + await assertDiagnostics(r''' +class B { + Future foo() async => 1; + Future bar() async => await foo() as int; + Future buzz() async => await bar(); +} +''', [ + lint(121, 5), + ]); + } + + test_asyncArrow_awaited_withAs() async { + await assertNoDiagnostics(r''' +class B { + Future foo() async => 1; + Future bar() async => await foo() as int; +} +'''); + } + + test_asyncArrow_notAwaited() async { + await assertNoDiagnostics(r''' +Future f() async => future; +final future = Future.value(1); +'''); + } + + test_asyncArrow_notAwaited_instanceMethod() async { + await assertNoDiagnostics(r''' +class A { + Future f() async => future; +} +final future = Future.value(1); +'''); + } + + test_asyncBlock_awaited() async { + await assertDiagnostics(r''' +Future f() async { + return await future; +} +final future = Future.value(1); +''', [ + lint(33, 5), + ]); + } + + test_asyncBlock_awaited_futureOfFuture() async { + await assertNoDiagnostics(r''' +Future f() async { + return await future; +} +final future = Future>.value(Future.value(1)); +'''); + } + + test_asyncBlock_awaited_instanceMethod() async { + await assertDiagnostics(r''' +class A { + Future f() async { + return await future; + } +} +final future = Future.value(1); +''', [ + lint(47, 5), + ]); + } + + test_asyncBlock_awaited_inTry() async { + await assertDiagnostics(r''' +Future f() async { + try { + return await future; + } catch (e) { + return await future; + } +} +final future = Future.value(1); +''', [ + lint(88, 5), + ]); + } + + test_asyncBlock_awaited_inTry_instanceMethod() async { + await assertDiagnostics(r''' +class A { + Future f() async { + try { + return await future; + } catch (e) { + return await future; + } + } +} +final future = Future.value(1); +''', [ + lint(108, 5), + ]); + } + + test_asyncBlock_notAwaited() async { + await assertNoDiagnostics(r''' +Future f() async { + return future; +} +final future = Future.value(1); +'''); + } + + test_asyncBlock_notAwaited_instanceMethod() async { + await assertNoDiagnostics(r''' +class A { + Future f() async { + return future; + } +} +final future = Future.value(1); +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_brace_in_string_interps_test.dart b/pkg/linter/test/rules/unnecessary_brace_in_string_interps_test.dart new file mode 100644 index 00000000000..055bfbf083b --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_brace_in_string_interps_test.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryBraceInStringInterpsTest); + }); +} + +@reflectiveTest +class UnnecessaryBraceInStringInterpsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_brace_in_string_interps'; + + test_simpleIdentifier() async { + await assertDiagnostics(r''' +void hi(String name) { + print('hi: ${name}'); +} +''', [ + lint(36, 7), + ]); + } + + test_simpleIdentifier_suffixed() async { + await assertNoDiagnostics(r''' +void hi(String name) { + print('hi: ${name}s'); +} +'''); + } + + test_this_methodInvocation() async { + await assertNoDiagnostics(r''' +class A { + void hi() { + print('hi: ${this.toString()}'); + } +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3691 + test_thisExpression() async { + await assertDiagnostics(r''' +class A { + void hi() { + print('hi: ${this}'); + } +} +''', [ + lint(39, 7), + ]); + } + + test_thisExpression_suffixed() async { + await assertNoDiagnostics(r''' +class A { + void hi() { + print('${this}s'); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_breaks_test.dart b/pkg/linter/test/rules/unnecessary_breaks_test.dart new file mode 100644 index 00000000000..6d55a7232a3 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_breaks_test.dart @@ -0,0 +1,108 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryBreaksTest); + }); +} + +@reflectiveTest +class UnnecessaryBreaksTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_breaks'; + + test_switch_pre30_ok() async { + await assertNoDiagnostics(r''' +// @dart=2.19 +f() { + switch (1) { + case 1: + f(); + break; + } +} +'''); + } + + test_switchPatternCase() async { + await assertDiagnostics(r''' +f() { + switch (1) { + case 1: + f(); + break; + case 2: + f(); + } +} +''', [ + lint(50, 6), + ]); + } + + test_switchPatternCase_empty_ok() async { + await assertNoDiagnostics(r''' +f() { + switch (1) { + case 1: + break; + case 2: + f(); + } +} +'''); + } + + test_switchPatternCase_labeled_ok() async { + await assertNoDiagnostics( + r''' +f() { + l: + switch (1) { + case 1: + break l; + case 2: + f(); + } +} +''', + ); + } + + test_switchPatternCase_notDirectChild_ok() async { + await assertNoDiagnostics(r''' +f(bool c) { + switch (1) { + case 1: + if (c) break; + f(true); + case 2: + f(true); + } +} +'''); + } + + test_switchPatternCase_notLast_ok() async { + await assertDiagnostics(r''' +f(bool c) { + switch (1) { + case 1: + break; + f(true); + case 2: + f(true); + } +} +''', [ + // No lint. + error(WarningCode.DEAD_CODE, 58, 8), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_const_test.dart b/pkg/linter/test/rules/unnecessary_const_test.dart new file mode 100644 index 00000000000..99f886b20be --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_const_test.dart @@ -0,0 +1,366 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryConstPatternsTest); + defineReflectiveTests(UnnecessaryConstRecordsTest); + }); +} + +@reflectiveTest +class UnnecessaryConstPatternsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_const'; + + test_case_constConstructor_ok() async { + await assertNoDiagnostics(r''' +class C { + const C(); +} +f(C c) { + switch (c) { + case const C(): + } +} +'''); + } + + test_case_listLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const [1, 2]: + } +} +'''); + } + + test_case_mapLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const {'k': 'v'}: + } +} +'''); + } + + test_case_setLiteral_ok() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case const {1}: + } +} +'''); + } + + test_constConstructor() async { + await assertDiagnostics(r''' +class C { + const C(); +} +const c = const C(); +''', [ + lint(35, 9), + ]); + } + + test_listLiteral() async { + await assertDiagnostics(r''' +const l = const []; +''', [ + lint(10, 8), + ]); + } + + test_mapLiteral() async { + await assertDiagnostics(r''' +const m = const {1: 1}; +''', [ + lint(10, 12), + ]); + } + + test_setLiteral() async { + await assertDiagnostics(r''' +const s = const {1}; +''', [ + lint(10, 9), + ]); + } +} + +@reflectiveTest +class UnnecessaryConstRecordsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_const'; + + test_constCall_nonConstArgument() async { + await assertNoDiagnostics(r''' +var x = const A({}); +class A { + const A(Object o); +} +'''); + } + + test_constVariable_constCall() async { + await assertDiagnostics(r''' +const x = const A(); +class A { + const A(); +} +''', [ + lint(10, 9), + ]); + } + + test_constVariable_constCall_newName_noArgument() async { + await assertDiagnostics(r''' +const x = const A.new(); +class A { + const A(); +} +''', [ + lint(10, 13), + ]); + } + + test_constVariable_constCall_nonConstArgument() async { + await assertDiagnostics(r''' +const x = const A([]); +class A { + const A(Object o); +} +''', [ + lint(10, 11), + ]); + } + + test_constVariable_nonConstCall() async { + await assertNoDiagnostics(r''' +const x = A(); +class A { + const A(); +} +'''); + } + + test_constVariable_nonConstCall_nonConstArgument() async { + await assertNoDiagnostics(r''' +const x = A([]); +class A { + const A(Object o); +} +'''); + } + + test_noContext_constCall() async { + await assertNoDiagnostics(r''' +void f() { + const A(); +} +class A { + const A(); +} +'''); + } + + test_noContext_constCall_newName() async { + await assertNoDiagnostics(r''' +void f() { + const A.new(); +} +class A { + const A(); +} +'''); + } + + test_noContext_constCall_nonConstArgument() async { + await assertNoDiagnostics(r''' +void f() { + const A(A()); +} +class A { + const A([o]); +} +'''); + } + + test_noContext_constCall_nonConstListArgument() async { + await assertNoDiagnostics(r''' +void f() { + const A([]); +} +class A { + const A(Object o); +} +'''); + } + + test_noContext_newName_nonConstCall() async { + await assertNoDiagnostics(r''' +void f() { + A.new(); +} +class A { + const A(); +} +'''); + } + + test_noContext_nonConstCall() async { + await assertNoDiagnostics(r''' +void f() { + A(); +} +class A { + const A(); +} +'''); + } + + test_noContext_nonConstCall_constListArgument() async { + await assertNoDiagnostics(r''' +void f() { + A(const []); +} +class A { + const A([o]); +} +'''); + } + + test_noContext_nonConstCall_constObjectArgument() async { + await assertNoDiagnostics(r''' +void f() { + A(const A()); +} +class A { + const A([o]); +} +'''); + } + + test_noContext_nonConstCall_nonConstArgument() async { + await assertNoDiagnostics(r''' +void f() { + A([]); +} +class A { + const A([o]); +} +'''); + } + + test_noContext_nonConstCall_nonConstObjectArgument() async { + await assertNoDiagnostics(r''' +void f() { + A(A()); +} +class A { + const A([o]); +} +'''); + } + + test_recordLiteral() async { + await assertDiagnostics(r''' +const r = const (a: 1); +''', [ + lint(10, 12), + ]); + } + + test_recordLiteral_ok() async { + await assertNoDiagnostics(r''' +const r = (a: 1); +'''); + } + + test_variable_constCall_constListArgument() async { + await assertDiagnostics(r''' +var x = const A(const []); +class A { + const A(Object o); +} +''', [ + lint(16, 8), + ]); + } + + test_variable_constCall_constSetArgument() async { + await assertDiagnostics(r''' +var x = const A(const {}); +class A { + const A(Object o); +} +''', [ + lint(16, 8), + ]); + } + + test_variable_constCall_newName_constArgument() async { + await assertDiagnostics(r''' +var x = const A.new(const []); +class A { + const A(Object o); +} +''', [ + lint(20, 8), + ]); + } + + test_variable_constCall_nonConstArgument() async { + await assertNoDiagnostics(r''' +final x = const A([]); +class A { + const A(Object o); +} +'''); + } + + test_variable_constCall_nonConstListArgument() async { + await assertNoDiagnostics(r''' +var x = const A([]); +class A { + const A(Object o); +} +'''); + } + + test_variable_nonConstCall() async { + await assertNoDiagnostics(r''' +var a = A(); +class A { + const A(); +} +'''); + } + + test_variable_nonConstCall_constListArgument() async { + await assertNoDiagnostics(r''' +var x = A(const []); +class A { + const A(Object o); +} +'''); + } + + test_variable_nonConstCall_newName_constArgument() async { + await assertNoDiagnostics(r''' +var x = A.new(const []); +class A { + const A(Object o); +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_constructor_name_test.dart b/pkg/linter/test/rules/unnecessary_constructor_name_test.dart new file mode 100644 index 00000000000..48363afee33 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_constructor_name_test.dart @@ -0,0 +1,118 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryConstructorNameTest); + }); +} + +@reflectiveTest +class UnnecessaryConstructorNameTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'unnecessary_constructor_name'; + + test_constructorDeclaration_named() async { + await assertNoDiagnostics(r''' +class A { + A.ok(); +} +'''); + } + + test_constructorDeclaration_new() async { + await assertDiagnostics(r''' +class A { + A.new(); +} +''', [ + lint(14, 3), + ]); + } + + @FailingTest( + reason: + 'https://github.com/dart-lang/linter/pull/4677#discussion_r1291784807') + test_constructorDeclaration_new_alreadyDefined() async { + await assertDiagnostics(r''' +class A { + A(); + A.new(); +} +''', [ + error(CompileTimeErrorCode.DUPLICATE_CONSTRUCTOR_DEFAULT, 19, 5), + // A lint should likely not get reported here since we're already + // producing a compilation error. + ]); + } + + test_constructorTearoff_new() async { + await assertNoDiagnostics(r''' +class A { +} +var makeA = A.new; +'''); + } + + test_extensionTypeDeclaration() async { + await assertDiagnostics(r''' +extension type E(int i) { + E.new(this.i); +} +''', [ + // No lint. + error(CompileTimeErrorCode.DUPLICATE_CONSTRUCTOR_DEFAULT, 28, 5), + ]); + } + + test_extensionTypeDeclaration_primaryNamed() async { + await assertDiagnostics(r''' +extension type E.a(int i) { + E.new(this.i); +} +''', [ + lint(32, 3), + ]); + } + + test_extensionTypeDeclaration_primaryNamedNew() async { + await assertDiagnostics(r''' +extension type E.new(int i) { } +''', [ + lint(17, 3), + ]); + } + + test_instanceCreation_named() async { + await assertNoDiagnostics(r''' +class A { + A.ok(); +} +var aaa = A.ok(); +'''); + } + + test_instanceCreation_new() async { + await assertDiagnostics(r''' +class A {} +var a = A.new(); +''', [ + lint(21, 3), + ]); + } + + test_instanceCreation_unnamed() async { + await assertNoDiagnostics(r''' +class A {} +var aa = A(); +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_final_test.dart b/pkg/linter/test/rules/unnecessary_final_test.dart new file mode 100644 index 00000000000..60a174f400c --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_final_test.dart @@ -0,0 +1,256 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryFinalTest); + }); +} + +@reflectiveTest +class UnnecessaryFinalTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_final'; + + test_field_final() async { + await assertNoDiagnostics(r''' +class C { + final int x = 3; +} +'''); + } + + test_forEachLoopVariable_final() async { + await assertDiagnostics(r''' +void f() { + for (final x in []) {} +} +''', [ + lint(18, 5), + ]); + } + + test_forEachLoopVariable_var() async { + await assertNoDiagnostics(r''' +void f() { + for (var x in []) {} +} +'''); + } + + test_listPattern_destructured() async { + await assertDiagnostics(r''' +f() { + final [a] = [1]; +} +''', [ + lint(8, 5), + ]); + } + + test_listPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case [int x, final int y]) {} +} +''', [ + lint(35, 5), + ]); + } + + test_localVariable_final() async { + await assertDiagnostics(r''' +void f() { + final x = 1; +} +''', [ + lint(13, 5), + ]); + } + + test_localVariable_var() async { + await assertNoDiagnostics(r''' +void f() { + var x = 1; +} +'''); + } + + test_mapPattern_destructured() async { + await assertDiagnostics(r''' +f() { + final {'a': a} = {'a': 1}; +} +''', [ + lint(8, 5), + ]); + } + + test_mapPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case {'x': final x, 'y' : var y}) {} +} +''', [ + lint(33, 5), + ]); + } + + test_objectPattern_destructured() async { + await assertDiagnostics(r''' +class C { + int c; + C(this.c); +} + +f() { + final C(:c) = C(1); +} +''', [ + lint(43, 5), + ]); + } + + test_objectPattern_destructured_ok() async { + await assertNoDiagnostics(r''' +class C { + int c; + C(this.c); +} + +f() { + var C(:c) = C(1); +} +'''); + } + + test_objectPattern_ifCase() async { + await assertDiagnostics(r''' +class C { + int c; + int d; + C(this.c, this.d); +} + +f(Object o) { + if (o case C(c: final x, d: var y)) {} +} +''', [ + lint(84, 5), + ]); + } + + test_objectPattern_switch() async { + await assertDiagnostics(r''' +class A { + int a; + A(this.a); +} + +f() { + switch (A(1)) { + case A(a: >0 && final b): + } +} +''', [ + lint(79, 5), + ]); + } + + test_parameter_function_finalTyped() async { + await assertDiagnostics(r''' +void f(final int x) {} +''', [ + lint(7, 5), + ]); + } + + test_parameter_function_typed() async { + await assertNoDiagnostics(r''' +void f(int x) { +} +'''); + } + + test_parameter_functionExpression_final() async { + await assertDiagnostics(r''' +void f() { + (final c) => c.length; +} +''', [ + lint(14, 5), + ]); + } + + test_parameter_functionExpression_typed() async { + await assertNoDiagnostics(r''' +void f() { + (String c) => c.length; +} +'''); + } + + test_recordPattern_destructured() async { + await assertDiagnostics(r''' +f() { + final (a, b) = (1, 2); +} +''', [ + lint(8, 5), + ]); + } + + test_recordPattern_destructured_forEach() async { + await assertDiagnostics(r''' +f() { + for (final (a, b) in [(1, 2)]) { } +} +''', [ + lint(13, 5), + ]); + } + + test_recordPattern_destructured_ok() async { + await assertNoDiagnostics(r''' +f() { + var (a, b) = (1, 2); +} +'''); + } + + test_recordPattern_ifCase() async { + await assertDiagnostics(r''' +f(Object o) { + if (o case (final int x, int y)) {} +} +''', [ + lint(28, 5), + ]); + } + + test_recordPattern_ifCase_ok() async { + await assertNoDiagnostics(r''' +f(Object o) { + if (o case (int x, int y)) {} +} +'''); + } + + test_recordPattern_switch() async { + await assertDiagnostics(r''' +f() { + switch ((1, 2)) { + case (final a, final b): + } +} +''', [ + lint(36, 5), + lint(45, 5), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_getters_setters_test.dart b/pkg/linter/test/rules/unnecessary_getters_setters_test.dart new file mode 100644 index 00000000000..deaee5aaae6 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_getters_setters_test.dart @@ -0,0 +1,126 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryGettersSettersTest); + }); +} + +@reflectiveTest +class UnnecessaryGettersSettersTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_getters_setters'; + + test_necessary_differentType() async { + await assertNoDiagnostics(r''' +class C { + String? _x; + + dynamic get x { + return _x; + } + set x(dynamic value) { + _x = value; + } +} +'''); + } + + test_necessary_hasAnnotation() async { + await assertNoDiagnostics(r''' +class C { + String? _x; + + @Annotation() + String? get x => _x; + set x(String? value) { + _x = value; + } +} + +class Annotation { + const Annotation(); +} +'''); + } + + test_necessary_nonTrivialSetter() async { + await assertNoDiagnostics(r''' +class C { + String? _x; + + String? get x => _x; + set x(String? value) { + _x = value?.toLowerCase(); + } +} +'''); + } + + test_necessary_nullAwareAssignment() async { + await assertNoDiagnostics(r''' +class C { + String? _x; + + String? get x => _x; + set x(String? value) { + _x ??= value; + } +} +'''); + } + + test_unnecessary_getterAndSetterHaveBlockBody() async { + await assertDiagnostics(r''' +class C { + String? _x; + + String? get x { + return _x; + } + set x(String? value) { + _x = value; + } +} +''', [ + lint(39, 1), + ]); + } + + test_unnecessary_getterHasExpressionBody() async { + await assertDiagnostics(r''' +class C { + String? _x; + + String? get x => _x; + set x(String? value) + { + _x = value; + } +} +''', [ + lint(39, 1), + ]); + } + + test_unnecessary_setterHasExpressionBody() async { + await assertDiagnostics(r''' +class C { + String? _x; + + String? get x { + return _x; + } + set x(String? value) => _x = value; +} +''', [ + lint(39, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_lambdas_test.dart b/pkg/linter/test/rules/unnecessary_lambdas_test.dart new file mode 100644 index 00000000000..a86a4f11d98 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_lambdas_test.dart @@ -0,0 +1,268 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryLambdasTest); + }); +} + +@reflectiveTest +class UnnecessaryLambdasTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_lambdas'; + + test_constructorCall_const() async { + await assertNoDiagnostics(r''' +class C { + const C(); +} +var x = [() => const C()]; +'''); + } + + test_constructorCall_matchingArg() async { + await assertDiagnostics(r''' +class C { + C(int i); +} +var x = [].map((x) => C(x)); +''', [ + lint(39, 11), + ]); + } + + test_constructorCall_multipleArgs() async { + await assertNoDiagnostics(r''' +class C { + C(int i, int j); +} +var x = [].map((x) => C(3, x)); +'''); + } + + test_constructorCall_noArgs() async { + await assertDiagnostics(r''' +class C {} +var x = [() => C()]; +''', [ + lint(20, 9), + ]); + } + + test_functionCall_explicitTypeArg() async { + await assertNoDiagnostics(r''' +void f() {} +var x = [() => f()]; +'''); + } + + test_constructorCall_noParameter_oneArg() async { + await assertNoDiagnostics(r''' +class C { + C(int i); +} +var x = [() => C(3)]; +'''); + } + + test_constructorCall_unrelatedArg() async { + await assertNoDiagnostics(r''' +class C { + C(int i); +} +var x = [].map((x) => C(3)); +'''); + } + + test_functionCall_matchingArg() async { + await assertDiagnostics(r''' +var x = [].forEach((x) => print(x)); +''', [ + lint(19, 15), + ]); + } + + test_asyncLambda() async { + await assertNoDiagnostics(r''' +var x = [].forEach((x) async => print(x)); +'''); + } + + test_functionTearoff() async { + await assertNoDiagnostics(r''' +var x = [].forEach(print); +'''); + } + + test_importedFunction() async { + newFile('$testPackageLibPath/b.dart', r''' +bool isB(Object o) => true; +'''); + await assertDiagnostics(r''' +import 'b.dart' as b; + +void f() { + [].where((o) => b.isB(o)); +} +''', [ + lint(45, 15), + ]); + } + + test_importedFunction_deferred() async { + newFile('$testPackageLibPath/b.dart', r''' +bool isB(Object o) => true; +'''); + await assertNoDiagnostics(r''' +import 'b.dart' deferred as b; + +void f() { + [].where((o) => b.isB(o)); +} +'''); + } + + test_matchingArg_dynamicParameterToOtherArg_named() async { + await assertNoDiagnostics(r''' +var x = f(fn: (s) => count(s)); +int count(String s) => s.length; +void f({int Function(dynamic)? fn}) {} +'''); + } + + @FailingTest(issue: 'https://github.com/dart-lang/linter/issues/3516') + test_matchingArg_dynamicParameterToOtherArg_positional() async { + await assertNoDiagnostics(r''' +var x = f((s) => count(s)); +int count(String s) => s.length; +void f(int Function(dynamic)? fn) {} +'''); + } + + test_methodCallOnFinalLocal_matchingArg() async { + await assertDiagnostics(r''' +void f() { + final l = []; + [].where((e) => l.contains(e)); +} +''', [ + lint(38, 20), + ]); + } + + test_methodCallOnLateFinalLocal_matchingArg() async { + await assertNoDiagnostics(r''' +void f() { + late final List l; + if (1 == 2) l = []; + [].where((e) => l.contains(e)); +} +'''); + } + + test_methodCallOnFinalLocal_closureParameterIsTarget() async { + await assertNoDiagnostics(r''' +void f() { + [].where((e) => e.contains(e)); +} +'''); + } + + test_emptyLambda() async { + await assertNoDiagnostics(r''' +var f = () {}; +'''); + } + + test_methodCallOnFinalLocal_closureParameterIsPartOfTarget() async { + await assertNoDiagnostics(r''' +void f() { + [].where((e) => (e + e).contains(e)); +} +'''); + } + + test_methodCallOnNonFinalLocal_matchingArg() async { + await assertNoDiagnostics(r''' +void f() { + var l = []; + [].where((e) => l.contains(e)); +} +'''); + } + + test_methodTearoffOnFinalLocal_matchingArg() async { + await assertNoDiagnostics(r''' +void f() { + final l = []; + [].where(l.contains); +} +'''); + } + + test_nonFunctionCall() async { + await assertNoDiagnostics(r''' +void f() { + final l = []; + [].where((e) => l.contains(e) || e is int); +} +'''); + } + + test_multipleStatements() async { + await assertNoDiagnostics(r''' +void f() { + [].forEach((e) { + print(e); + print(e); + }); +} +'''); + } + + test_functionCall_singleStatement() async { + await assertDiagnostics(r''' +final f = () {}; +final l = () { + f(); +}; +''', [ + lint(27, 13), + ]); + } + + test_noParameter_targetIsFinalField() async { + await assertDiagnostics(r''' +class C { + final f = 1; + Function m() { + return () { + f.toString(); + }; + } +} +''', [ + lint(53, 30), + ]); + } + + test_noParameter_targetIsGetter() async { + await assertNoDiagnostics(r''' +class C { + get f => 2; + + Function m() { + return () { + f.toString(); + }; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_late_test.dart b/pkg/linter/test/rules/unnecessary_late_test.dart new file mode 100644 index 00000000000..9f44c0b2f43 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_late_test.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryLateTest); + }); +} + +@reflectiveTest +class UnnecessaryLateTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_late'; + + test_local_initializer() async { + await assertNoDiagnostics(r''' +class C { + void f() { + late String a = ''; + } +} +'''); + } + + test_multipleVariables_eachHasInitializer() async { + await assertDiagnostics(r''' +late String a = '', + b = ''; +''', [ + lint(12, 1), + lint(24, 1), + ]); + } + + test_multipleVariables_oneHasInitializer_oneHasNoInitializer() async { + await assertDiagnostics(r''' +late String a, + b = ''; +''', [ + lint(19, 1), + ]); + } + + test_static_initializer() async { + await assertDiagnostics(r''' +class C { + static late String a = ''; +} +''', [ + lint(31, 1), + ]); + } + + test_static_noInitializer() async { + await assertNoDiagnostics(r''' +class C { + static late String a; +} +'''); + } + + test_static_nonLate() async { + await assertNoDiagnostics(r''' +class C { + static String a = ''; +} +'''); + } + + test_topLevel_initializer() async { + await assertDiagnostics(r''' +late String a = ''; +''', [ + lint(12, 1), + ]); + } + + test_topLevel_noInitializer() async { + await assertNoDiagnostics(r''' +late String a; +'''); + } + + test_topLevel_noLate() async { + await assertNoDiagnostics(r''' +String a = ''; +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_library_directive_test.dart b/pkg/linter/test/rules/unnecessary_library_directive_test.dart new file mode 100644 index 00000000000..02704d001b1 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_library_directive_test.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryLibraryDirective); + }); +} + +@reflectiveTest +class UnnecessaryLibraryDirective extends LintRuleTest { + @override + String get lintRule => 'unnecessary_library_directive'; + + test_hasAnnotation() async { + await assertNoDiagnostics(r''' +@C() +library with_annotation; + +class C { + const C(); +} +'''); + } + + test_hasDocComment() async { + await assertNoDiagnostics(r''' +/// This is a nice library. +library with_comment; +'''); + } + + test_hasPart() async { + newFile2('$testPackageLibPath/part.dart', ''' +part of 'test.dart'; +'''); + // Parts may still use library names to reference what they are a 'part of'. + // We don't lint those libraries, even though using library names in + // 'part of' is discouraged. + await assertNoDiagnostics(r''' +library lib; + +part 'part.dart'; +'''); + } + + test_unnecessary() async { + await assertDiagnostics(r''' +library lib; +''', [ + lint(0, 12), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_new_test.dart b/pkg/linter/test/rules/unnecessary_new_test.dart new file mode 100644 index 00000000000..072f25a79ab --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_new_test.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNewTest); + }); +} + +@reflectiveTest +class UnnecessaryNewTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_new'; + + test_named_new() async { + await assertDiagnostics(r''' +class A { + A.named(); +} + +void f() { + new A.named(); +} +''', [ + lint(39, 13), + ]); + } + + test_named_noNew() async { + await assertNoDiagnostics(r''' +class A { + A.named(); +} + +void f() { + A.named(); +} +'''); + } + + test_unnamed_const() async { + await assertNoDiagnostics(r''' +class A { + const A(); +} + +void f() { + const A(); +} +'''); + } + + test_unnamed_new() async { + await assertDiagnostics(r''' +class A { + const A(); +} + +void f() { + new A(); +} +''', [ + lint(39, 7), + ]); + } + + test_unnamed_newName_const() async { + await assertNoDiagnostics(r''' +class A { + const A(); +} + +void f() { + const A.new(); +} +'''); + } + + test_unnamed_newName_new() async { + await assertDiagnostics(r''' +class A { + const A(); +} + +void f() { + new A.new(); +} +''', [ + lint(39, 11), + ]); + } + + test_unnamed_noNew() async { + await assertNoDiagnostics(r''' +class A { + const A(); +} + +void f() { + A(); +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_null_aware_assignments_test.dart b/pkg/linter/test/rules/unnecessary_null_aware_assignments_test.dart new file mode 100644 index 00000000000..f9b90df7232 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_null_aware_assignments_test.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNullAwareAssignmentsTest); + }); +} + +@reflectiveTest +class UnnecessaryNullAwareAssignmentsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_null_aware_assignments'; + + test_explicitSetter() async { + await assertNoDiagnostics(r''' +int? get x => null; +set x(int? x) {} + +void f() { + x ??= null; +} +'''); + } + + test_localVariable_nullAssignment() async { + await assertDiagnostics(r''' + void f() { + var x; + x ??= null; + } +''', [ + lint(28, 10), + ]); + } + + test_localVariable_otherAssignment() async { + await assertNoDiagnostics(r''' + void f() { + var x; + x ??= 1; + } +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_null_aware_operator_on_extension_on_nullable_test.dart b/pkg/linter/test/rules/unnecessary_null_aware_operator_on_extension_on_nullable_test.dart new file mode 100644 index 00000000000..6fa8490cac6 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_null_aware_operator_on_extension_on_nullable_test.dart @@ -0,0 +1,262 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests( + UnnecessaryNullAwareOperatorOnExtensionOnNullableTest); + }); +} + +@reflectiveTest +class UnnecessaryNullAwareOperatorOnExtensionOnNullableTest + extends LintRuleTest { + @override + String get lintRule => + 'unnecessary_null_aware_operator_on_extension_on_nullable'; + + test_extensionOverride_getter() async { + await assertNoDiagnostics(r''' +extension E on int? { + int get foo => 1; +} +void f(int? i) { + E(i).foo; +} +'''); + } + + test_extensionOverride_getter_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + int get foo => 1; +} +void f(int? i) { + E(i)?.foo; +} +''', [ + lint(67, 2), + ]); + } + + test_extensionOverride_indexAssignment() async { + await assertNoDiagnostics(r''' +extension E on int? { + void operator []=(int i, String v) {} +} +void f(int? i) { + E(i)[0] = ''; +} +'''); + } + + test_extensionOverride_indexAssignment_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + void operator []=(int i, String v) {} +} +void f(int? i) { + E(i)?[0] = ''; +} +''', [ + lint(87, 1), + ]); + } + + test_extensionOverride_indexOperator() async { + await assertNoDiagnostics(r''' +extension E on int? { + String operator [](int i) => ''; +} +void f(int? i) { + E(i)[0]; +} +'''); + } + + test_extensionOverride_indexOperator_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + String operator [](int i) => ''; +} +void f(int? i) { + E(i)?[0]; +} +''', [ + lint(82, 1), + ]); + } + + test_extensionOverride_methodCall() async { + await assertNoDiagnostics(r''' +extension E on int? { + int m() => 1; +} +void f(int? i) { + E(i).m(); +} +'''); + } + + test_extensionOverride_methodCall_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + int m() => 1; +} +void f(int? i) { + E(i)?.m(); +} +''', [ + lint(63, 2), + ]); + } + + test_extensionOverride_setter() async { + await assertNoDiagnostics(r''' +extension E on int? { + void set foo(int v) {} +} +void f(int? i) { + E(i).foo = 1; +} +'''); + } + + test_extensionOverride_setter_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + void set foo(int v) {} +} +void f(int? i) { + E(i)?.foo = 1; +} +''', [ + lint(72, 2), + ]); + } + + test_getter() async { + await assertNoDiagnostics(r''' +extension E on int? { + int get foo => 1; +} +void f(int? i) { + i.foo; +} +'''); + } + + test_getter_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + int get foo => 1; +} +void f(int? i) { + i?.foo; +} +''', [ + lint(64, 2), + ]); + } + + test_indexAssignment() async { + await assertNoDiagnostics(r''' +extension E on int? { + void operator []=(int i, String v) {} +} +void f(int? i) { + i[0] = ''; +} +'''); + } + + test_indexAssignment_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + void operator []=(int i, String v) {} +} +void f(int? i) { + i?[0] = ''; +} +''', [ + lint(84, 1), + ]); + } + + test_indexOperator() async { + await assertNoDiagnostics(r''' +extension E on int? { + String operator [](int i) => ''; +} +void f(int? i) { + i[0]; +} +'''); + } + + test_indexOperator_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + String operator [](int i) => ''; +} +void f(int? i) { + i?[0]; +} +''', [ + lint(79, 1), + ]); + } + + test_methodCall() async { + await assertNoDiagnostics(r''' +extension E on int? { + int m() => 1; +} +void f(int? i) { + i.m(); +} +'''); + } + + test_methodCall_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + int m() => 1; +} +void f(int? i) { + i?.m(); +} +''', [ + lint(60, 2), + ]); + } + + test_setter() async { + await assertNoDiagnostics(r''' +extension E on int? { + void set foo(int v) {} +} +void f(int? i) { + i.foo = 1; +} +'''); + } + + test_setter_nullAware() async { + await assertDiagnostics(r''' +extension E on int? { + void set foo(int v) {} +} +void f(int? i) { + i?.foo = 1; +} +''', [ + lint(69, 2), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_null_checks_test.dart b/pkg/linter/test/rules/unnecessary_null_checks_test.dart new file mode 100644 index 00000000000..81f4673862b --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_null_checks_test.dart @@ -0,0 +1,383 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNullChecksTest); + }); +} + +@reflectiveTest +class UnnecessaryNullChecksTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_null_checks'; + + test_assignment_await_nullableTarget() async { + await assertDiagnostics(r''' +Future f(int? p, int? i, Future future) async { + i = await future!; +} +''', [ + error(StaticWarningCode.UNNECESSARY_NON_NULL_ASSERTION, 78, 1), + lint(78, 1), + ]); + } + + test_assignment_nullable() async { + await assertDiagnostics(r''' +void f(int? v, int? i) { + v = i!; +} +''', [ + lint(32, 1), + ]); + } + + test_assignment_nullable_self() async { + await assertNoDiagnostics(r''' +void f(int? v) { + int? v; + v = v!; +} +'''); + } + + test_assignment_nullableTarget() async { + await assertDiagnostics(r''' +int? j = i!; +int? i; +''', [ + lint(10, 1), + ]); + } + + test_assignment_nullableTarget_parenthesized() async { + await assertDiagnostics(r''' +int? j2 = (i!); +int? i; +''', [ + lint(12, 1), + ]); + } + + test_binaryOperator_argument() async { + await assertDiagnostics(r''' +class A { + operator +(int? p) => A() + p!; +} +''', [ + lint(41, 1), + ]); + } + + test_completerComplete() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f(int? i) => Completer().complete(i!); +'''); + } + + test_compoundAssignment_promotedToNonNullable() async { + await assertNoDiagnostics(r''' +void f(int? v, int? p) { + v ??= 1; + v += p!; +} +'''); + } + + test_constructorCall_optionalPositionalArgument() async { + await assertDiagnostics(r''' +class A { + A([int? p]); +} +void f(int? i) => A(i!); +''', [ + lint(48, 1), + ]); + } + + test_functionCall_namedArgument() async { + await assertDiagnostics(r''' +void f({int? p, int? i}) => f(p: i!); +''', [ + lint(34, 1), + ]); + } + + test_functionCall_positionalArgument() async { + await assertDiagnostics(r''' +int? i; +void f(int? p) => f(i!); +''', [ + lint(29, 1), + ]); + } + + test_functionCall_positionalArgument_parenthesized() async { + await assertDiagnostics(r''' +int? i; +void f(int? p) => f((i!)); +''', [ + lint(30, 1), + ]); + } + + test_futureValue() async { + await assertNoDiagnostics(r''' +void f(int? i) => Future.value(i!); +'''); + } + + test_listPattern() async { + await assertDiagnostics(r''' +void f(int? a, int? b) { + [b!, ] = [a, ]; +} +''', [ + lint(29, 1), + ]); + } + + test_nullableAssignment_nullable() async { + await assertNoDiagnostics(r''' +void f(int? v, int? p) { + v ??= p!; +} +'''); + } + + test_recordPattern() async { + await assertDiagnostics(r''' +void f(int? a, int? b) { + (b!, ) = (a, ); +} +''', [ + lint(29, 1), + ]); + } + + test_return_function_dynamic() async { + // TODO(srawlins): Why does a dynamic-returning function result in a + // diagnostic, but a dynamic-returning method does not? + await assertDiagnostics(r''' +dynamic f(int? p) { + return p!; +} +''', [ + lint(30, 1), + ]); + } + + test_return_list_nonNullable() async { + await assertNoDiagnostics(r''' +List f7ok(int? p) => [p!]; +'''); + } + + test_return_list_nullable() async { + await assertDiagnostics(r''' +List f7(int? p) => [p!]; +''', [ + lint(27, 1), + ]); + } + + test_return_mapKey_nonNullable() async { + await assertNoDiagnostics(r''' +Map f(int? p) => {p!: ''}; +'''); + } + + test_return_mapKey_nullable() async { + await assertDiagnostics(r''' +Map f(int? p) => {p!: ''}; +''', [ + lint(33, 1), + ]); + } + + test_return_mapValue_forElement_nonNullable() async { + await assertNoDiagnostics(r''' +Map f(int? p) => {for (var a in []) '': p!}; +'''); + } + + test_return_mapValue_forElement_nullable() async { + await assertDiagnostics(r''' +Map f(int? p) => {for (var a in []) '': p!}; +''', [ + lint(60, 1), + ]); + } + + test_return_mapValue_ifElement_nonNullable() async { + await assertNoDiagnostics(r''' +Map f(int? p) => {if (1 != 0) '': p!}; // OK +'''); + } + + test_return_mapValue_ifElement_nullable() async { + await assertDiagnostics(r''' +Map f(int? p) => {if (1 != 0) '': p!}; +''', [ + lint(49, 1), + ]); + } + + test_return_mapValue_ifElementNested_nullable() async { + await assertDiagnostics(r''' +Map f(int? p) => {if (1 != 0) if (1 != 0) '': p!}; +''', [ + lint(61, 1), + ]); + } + + test_return_mapValue_nonNullable() async { + await assertNoDiagnostics(r''' +Map f(int? p) => {'': p!}; +'''); + } + + test_return_mapValue_nullable() async { + await assertDiagnostics(r''' +Map f(int? p) => {'': p!}; +''', [ + lint(37, 1), + ]); + } + + test_return_method_dynamic() async { + await assertNoDiagnostics(r''' +class A { + dynamic f(int? p) { + return p!; + } +} +'''); + } + + test_return_nullable() async { + await assertDiagnostics(r''' +int? f(int? i) { return i!; } +''', [ + lint(25, 1), + ]); + } + + test_return_set_nonNullable() async { + await assertNoDiagnostics(r''' +Set f(int? p) => {p!}; +'''); + } + + test_return_set_nullable() async { + await assertDiagnostics(r''' +Set f(int? p) => {p!}; +''', [ + lint(25, 1), + ]); + } + + test_returnAsync_dynamic() async { + await assertNoDiagnostics(r''' +dynamic f(int? p) async => p!; +'''); + } + + test_returnAsync_futureOfNonNullable() async { + await assertNoDiagnostics(r''' +Future f15ok(int? p) async => p!; +'''); + } + + test_returnAsync_futureOfNonNullable_await() async { + await assertNoDiagnostics(r''' +Future f(int? p) async => await p!; +'''); + } + + test_returnAsync_futureOfNonNullable_typedef() async { + await assertNoDiagnostics(r''' +typedef F = Future; +F f(int? p) async => p!; +'''); + } + + test_returnAsync_futureOfNullable() async { + await assertDiagnostics(r''' +Future f(int? p) async => p!; +''', [ + lint(33, 1), + ]); + } + + test_returnAsync_futureOfNullable_await() async { + await assertDiagnostics(r''' +Future f(int? p) async => await p!; +''', [ + lint(39, 1), + ]); + } + + test_returnAsync_futureOfNullable_typedef() async { + await assertDiagnostics(r''' +typedef F = Future; +F f(int? p) async => p!; +''', [ + lint(48, 1), + ]); + } + + test_returnExpressionBody_nullable() async { + await assertDiagnostics(r''' +int? f(int? i) => i!; +''', [ + lint(19, 1), + ]); + } + + test_undefinedFunction() async { + await assertDiagnostics(r''' +f6(int? p) { + return B() + p!; // OK +} +''', [ + // No lint + error(CompileTimeErrorCode.UNDEFINED_FUNCTION, 22, 1), + ]); + } + + test_yieldAsyncStar_streamOfNonNullable() async { + await assertNoDiagnostics(r''' +Stream f13ok(int? p) async* {yield p!;} +'''); + } + + test_yieldAsyncStar_streamOfNullable() async { + await assertDiagnostics(r''' +Stream f(int? p) async* {yield p!;} +''', [ + lint(38, 1), + ]); + } + + test_yieldSyncStar_iterableOfNonNullable() async { + await assertNoDiagnostics(r''' +Iterable f(int? p) sync* {yield p!;} +'''); + } + + test_yieldSyncStar_iterableOfNullable() async { + await assertDiagnostics(r''' +Iterable f(int? p) sync* {yield p!;} +''', [ + lint(39, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_null_in_if_null_operators_test.dart b/pkg/linter/test/rules/unnecessary_null_in_if_null_operators_test.dart new file mode 100644 index 00000000000..085eb253bcd --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_null_in_if_null_operators_test.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNullInIfNullOperatorsLanguage29Test); + }); +} + +@reflectiveTest +class UnnecessaryNullInIfNullOperatorsLanguage29Test extends LintRuleTest { + @override + String get lintRule => 'unnecessary_null_in_if_null_operators'; + + @override + String get testPackageLanguageVersion => '2.9'; + + test_localVariableDeclaration_noNull() async { + await assertNoDiagnostics(r''' +void f() { + var x = 1 ?? 1; +} +'''); + } + + test_localVariableDeclaration_nullOnLeft() async { + await assertDiagnostics(r''' +void f() { + var x = null ?? 1; +} +''', [ + lint(21, 9), + ]); + } + + test_localVariableDeclaration_nullOnRight() async { + await assertDiagnostics(r''' +void f() { + var x = 1 ?? null; +} +''', [ + lint(21, 9), + ]); + } + + test_topLevelVariableDeclaration_noNull() async { + await assertNoDiagnostics(r''' +var x = 1 ?? 1; +'''); + } + + test_topLevelVariableDeclaration_nullOnLeft() async { + await assertDiagnostics(r''' +var x = null ?? 1; +''', [ + lint(8, 9), + ]); + } + + test_topLevelVariableDeclaration_nullOnRight() async { + await assertDiagnostics(r''' +var x = 1 ?? null; +''', [ + lint(8, 9), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_nullable_for_final_variable_declarations_test.dart b/pkg/linter/test/rules/unnecessary_nullable_for_final_variable_declarations_test.dart new file mode 100644 index 00000000000..ce9b6072f19 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_nullable_for_final_variable_declarations_test.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNullableForFinalVariableDeclarationsTest); + }); +} + +@reflectiveTest +class UnnecessaryNullableForFinalVariableDeclarationsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_nullable_for_final_variable_declarations'; + + test_list() async { + await assertDiagnostics(r''' +f() { + final [int a, num? c] = [0, 1]; + print('$a$c'); +} +''', [ + lint(22, 6), + ]); + } + + test_list_dynamic_ok() async { + await assertNoDiagnostics(r''' +f() { + final [dynamic a, num c] = [0, 1]; + print('$a$c'); +} +'''); + } + + test_nonNullableType() async { + await assertNoDiagnostics(r''' +final int i = 1; +'''); + } + + test_record() async { + await assertDiagnostics(r''' +f() { + final (List? a, num? c) = ([], 1); + print('$a$c'); +} +''', [ + lint(15, 12), + lint(29, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_overrides_test.dart b/pkg/linter/test/rules/unnecessary_overrides_test.dart new file mode 100644 index 00000000000..110cd3d4841 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_overrides_test.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryOverridesTest); + }); +} + +@reflectiveTest +class UnnecessaryOverridesTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_overrides'; + + test_enum_field() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + @override + Type get runtimeType => super.runtimeType; +} +''', [ + lint(41, 11), + ]); + } + + test_enum_method() async { + await assertDiagnostics(r''' +enum A { + a,b,c; + @override + String toString() => super.toString(); +} +''', [ + lint(39, 8), + ]); + } + + test_method_ok_commentsInBody() async { + await assertNoDiagnostics(r''' +class A { + void a() { } +} + +class B extends A { + @override + void a() { + // There's something we want to document here. + super.a(); + } +} +'''); + } + + test_method_ok_expressionStatement_commentsInBody() async { + await assertNoDiagnostics(r''' +class A { + void a() { } +} + +class B extends A { + @override + void a() => + // There's something we want to document here. + super.a(); +} +'''); + } + + test_method_ok_returnExpression_commentsInBody() async { + await assertNoDiagnostics(r''' +class A { + @override + String toString() { + // There's something we want to document here. + return super.toString(); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_parenthesis_test.dart b/pkg/linter/test/rules/unnecessary_parenthesis_test.dart new file mode 100644 index 00000000000..3f647bd4ab2 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_parenthesis_test.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryParenthesisTest); + defineReflectiveTests(UnnecessaryParenthesisPatternsTest); + }); +} + +@reflectiveTest +class UnnecessaryParenthesisPatternsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_parenthesis'; + + /// https://github.com/dart-lang/linter/issues/4060 + test_constantPattern() async { + await assertNoDiagnostics(r''' +const a = 1; +const b = 2; + +void f(int i) { + switch (i) { + case const (a + b): + } +} +'''); + } +} + +@reflectiveTest +class UnnecessaryParenthesisTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_parenthesis'; + + /// https://github.com/dart-lang/linter/issues/4041 + test_nullAware_cascadeAssignment() async { + await assertNoDiagnostics(r''' +class A { + var b = false; + void m() {} + set setter(int i) {} +} + +void f(A? a) { + (a?..b = true)?.m(); + (a?..b = true)?.setter = 1; +} + +void g(List? list) { + (list?..[0] = 1)?.length; +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_raw_strings_test.dart b/pkg/linter/test/rules/unnecessary_raw_strings_test.dart new file mode 100644 index 00000000000..cbc3cbefb99 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_raw_strings_test.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryRawStringsTest); + }); +} + +@reflectiveTest +class UnnecessaryRawStringsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_raw_strings'; + + test_doubleQuotes_raw() async { + await assertDiagnostics(r''' +var s = r"a b c d"; +''', [ + lint(8, 10), + ]); + } + + test_doubleQuotes_raw_containsBackslash() async { + await assertNoDiagnostics(r''' +var s = r"a b c\d"; +'''); + } + + test_doubleQuotes_raw_containsDollar() async { + await assertNoDiagnostics(r''' +var s = r"a b c$d"; +'''); + } + + test_singleQuote() async { + await assertNoDiagnostics(r''' +var s = 'a b c d'; +'''); + } + + test_singleQuote_raw() async { + await assertDiagnostics(r''' +var s = r'a b c d'; +''', [ + lint(8, 10), + ]); + } + + test_singleQuote_raw_containsBackslash() async { + await assertNoDiagnostics(r''' +var s = r'a b c\d'; +'''); + } + + test_singleQuote_raw_containsDollar() async { + await assertNoDiagnostics(r''' +var s = r'a b c$d'; +'''); + } + + test_tripleDoubleQuotes_raw() async { + await assertDiagnostics(r''' +var s = r"""a b c d"""; +''', [ + lint(8, 14), + ]); + } + + test_tripleDoubleQuotes_raw_containsBackslash() async { + await assertNoDiagnostics(r''' +var s = r"""a b c\d"""; +'''); + } + + test_tripleDoubleQuotes_raw_containsDollar() async { + await assertNoDiagnostics(r''' +var s = r"""a b c$d"""; +'''); + } + + test_tripleSingleQuote_raw() async { + await assertDiagnostics(r""" +var s = r'''a b c d'''; +""", [ + lint(8, 14), + ]); + } + + test_tripleSingleQuote_raw_containsBackslash() async { + await assertNoDiagnostics(r""" +var s = r'''a b c\d'''; +"""); + } + + test_tripleSingleQuote_raw_containsDollar() async { + await assertNoDiagnostics(r""" +var s = r'''a b c$d'''; +"""); + } +} diff --git a/pkg/linter/test/rules/unnecessary_statements_test.dart b/pkg/linter/test/rules/unnecessary_statements_test.dart new file mode 100644 index 00000000000..68263043c16 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_statements_test.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryStatementsTest); + }); +} + +@reflectiveTest +class UnnecessaryStatementsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_statements'; + + /// https://github.com/dart-lang/linter/issues/4334 + test_patternAssignment_ok() async { + await assertNoDiagnostics(r''' +f() { + var (a, b) = (0, 0); + var result = (1, 2); + (a, b) = (a + result.$1, b + result.$2); +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_string_escapes_test.dart b/pkg/linter/test/rules/unnecessary_string_escapes_test.dart new file mode 100644 index 00000000000..1b75185b136 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_string_escapes_test.dart @@ -0,0 +1,210 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryStringEscapesTest); + }); +} + +@reflectiveTest +class UnnecessaryStringEscapesTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_string_escapes'; + + test_escapedBackslash() async { + await assertNoDiagnostics(r''' +var x = '\\'; +'''); + } + + test_escapedColon() async { + await assertDiagnostics(r''' +var x = '\:'; +''', [ + lint(9, 1), + ]); + } + + test_escapedDoubleQuotes_inDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = "\""; +'''); + } + + test_escapedDoubleQuotes_inSingleQuotes() async { + await assertDiagnostics(r''' +var x = '\"'; +''', [ + lint(9, 1), + ]); + } + + test_escapedDoubleQuotes_inSingleQuotes_raw() async { + await assertNoDiagnostics(r''' +var x = r'\"'; +'''); + } + + test_escapedDoubleQuotes_inThreeDoubleQuotes() async { + await assertDiagnostics(r''' +var x = """\"text"""; +''', [ + lint(11, 1), + ]); + } + + test_escapedDoubleQuotes_inThreeDoubleQuotes_atEnd() async { + await assertNoDiagnostics(r''' +var x = """text\""""; +'''); + } + + test_escapedDoubleQuotes_inThreeSingleQuotes() async { + await assertDiagnostics(r""" +var x = '''\"'''; +""", [ + lint(11, 1), + ]); + } + + test_escapedLowerA() async { + await assertDiagnostics(r''' +var x = '\a'; +''', [ + lint(9, 1), + ]); + } + + test_escapedLowerDollar() async { + await assertNoDiagnostics(r''' +var x = '\$'; +'''); + } + + test_escapedLowerN() async { + await assertNoDiagnostics(r''' +var x = '\n'; +'''); + } + + test_escapedLowerR() async { + await assertNoDiagnostics(r''' +var x = '\r'; +'''); + } + + test_escapedLowerT() async { + await assertNoDiagnostics(r''' +var x = '\t'; +'''); + } + + test_escapedSingleQuote_inDoubleQuotes() async { + await assertDiagnostics(r''' +var x = "\'"; +''', [ + lint(9, 1), + ]); + } + + test_escapedSingleQuote_inDoubleQuotes_raw() async { + await assertNoDiagnostics(r''' +var x = r"\'"; +'''); + } + + test_escapedSingleQuote_inSingleQuotes() async { + await assertNoDiagnostics(r''' +var x = '\''; +'''); + } + + test_escapedSingleQuote_inThreeDoubleQuotes() async { + await assertDiagnostics(r''' +var x = """\'"""; +''', [ + lint(11, 1), + ]); + } + + test_escapedSingleQuote_inThreeSingleQuotes() async { + await assertDiagnostics(r""" +var x = '''\'text'''; +""", [ + lint(11, 1), + ]); + } + + test_threeEscapedSingleQuote_inThreeSingleQuotes() async { + await assertNoDiagnostics(r""" +var x = '''text\'\'\'text'''; +"""); + } + + test_moreThanThreeEscapedSingleQuote_inThreeSingleQuotes() async { + await assertNoDiagnostics(r""" +var x = '''text\'\'\'\'\'\'text'''; +"""); + } + + test_escapedSingleQuote_inThree_inThreeSingleQuotes() async { + await assertNoDiagnostics(r""" +var x = '''text'\''text'''; +"""); + } + + test_threeEscapedDoubleQuote_inThreeDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = """text\"\"\"text"""; +'''); + } + + test_moreThanThreeEscapedDoubleQuotes_inThreeDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = """text\"\"\"\"\"\"text"""; +'''); + } + + test_escapedDoubleQuotes_inThree_inThreeDoubleQuotes() async { + await assertNoDiagnostics(r''' +var x = """text"\""text"""; +'''); + } + + test_escapedSingleQuote_inThreeSingleQuotes_atEnd() async { + await assertNoDiagnostics(r""" +var x = '''text\''''; +"""); + } + + test_escapedUtfHex() async { + await assertNoDiagnostics(r''' +var x = '\uFFFF'; +'''); + } + + test_escapedXHex() async { + await assertNoDiagnostics(r''' +var x = '\x00'; +'''); + } + + test_unterminatedStringLiteral() async { + // Note that putting `''` on the new line is important to get a token + // with `'\` with no closing quote. + await assertDiagnostics(r''' +String unclosedQuote() => '\ +''; +''', [ + // Ensure linter does not crash. + error(ParserErrorCode.INVALID_UNICODE_ESCAPE_STARTED, 27, 1), + error(ScannerErrorCode.UNTERMINATED_STRING_LITERAL, 27, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_string_interpolations_test.dart b/pkg/linter/test/rules/unnecessary_string_interpolations_test.dart new file mode 100644 index 00000000000..ee7d340549c --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_string_interpolations_test.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryStringInterpolationsTest); + }); +} + +@reflectiveTest +class UnnecessaryStringInterpolationsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_string_interpolations'; + + test_necessaryInterpolation_adjacentStrings() async { + await assertNoDiagnostics(r''' +var a = ''; +var b = 'x' '$a'; +'''); + } + + test_necessaryInterpolation_nullableString() async { + await assertNoDiagnostics(r''' +class Node { + String? x = ''; + String f() => '$x'; +} +'''); + } + + test_necessaryInterpolation_property() async { + await assertNoDiagnostics(r''' +var a = ''; +var b = '${a.length}'; +'''); + } + + test_necessaryInterpolation_single() async { + await assertNoDiagnostics(r''' +var a = ''; +var b = 'x$a'; +'''); + } + + test_necessaryInterpolation_triple() async { + await assertNoDiagnostics(r""" +var a = ''; +var b = '''x$a'''; +"""); + } + + test_unnecessaryInterpolation_single() async { + await assertDiagnostics(r''' +var a = ''; +var b = '$a'; +''', [ + lint(20, 4), + ]); + } + + test_unnecessaryInterpolation_substring() async { + await assertDiagnostics(r''' +var a = ''; +var b = '${a.substring(1)}'; +''', [ + lint(20, 19), + ]); + } + + test_unnecessaryInterpolation_triple() async { + await assertDiagnostics(r""" +var a = ''; +var b = '''$a'''; +""", [ + lint(20, 8), + ]); + } + + test_unnecessaryInterpolation_withBraces() async { + await assertDiagnostics(r''' +var a = ''; +var b = '${a}'; +''', [ + lint(20, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/unnecessary_this_test.dart b/pkg/linter/test/rules/unnecessary_this_test.dart new file mode 100644 index 00000000000..9a59362ff03 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_this_test.dart @@ -0,0 +1,72 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryNullChecksTest); + }); +} + +@reflectiveTest +class UnnecessaryNullChecksTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'unnecessary_this'; + + test_extensionType_inConstructorInitializer() async { + await assertDiagnostics(r''' +extension type E(int i) { + E.e(int i) : this.i = i.hashCode; +} +''', [ + lint(41, 4), + ]); + } + + test_extensionType_inMethod() async { + await assertDiagnostics(r''' +extension type E(Object o) { + String m()=> this.toString(); +} +''', [ + lint(44, 15), + ]); + } + + test_shadowInObjectPattern() async { + await assertNoDiagnostics(r''' +class C { + Object? value; + bool equals(Object other) => + switch (other) { C(:var value) => this.value == value, _ => false }; +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4457 + @FailingTest(issue: 'https://github.com/dart-lang/linter/issues/4457') + test_shadowSwitchPatternCase() async { + await assertNoDiagnostics(r''' +class C { + String? name; + + void m(bool b) { + switch (b) { + case true: + var name = this.name!; + print(name); + case false: + break; + } + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/unnecessary_to_list_in_spreads_test.dart b/pkg/linter/test/rules/unnecessary_to_list_in_spreads_test.dart new file mode 100644 index 00000000000..c5b50c1e3a6 --- /dev/null +++ b/pkg/linter/test/rules/unnecessary_to_list_in_spreads_test.dart @@ -0,0 +1,81 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnnecessaryToListInSpreadsTest); + }); +} + +@reflectiveTest +class UnnecessaryToListInSpreadsTest extends LintRuleTest { + @override + String get lintRule => 'unnecessary_to_list_in_spreads'; + + test_iterableToList() async { + await assertDiagnostics(r''' +var x = [ + ...[1, 2].whereType().toList(), +]; +''', [ + lint(39, 6), + ]); + } + + test_listToList() async { + await assertDiagnostics(r''' +var x = [ + ...[1, 2].toList(), +]; +''', [ + lint(22, 6), + ]); + } + + test_listToList_nullAwareSpread() async { + await assertDiagnostics(r''' +void f(List? p) { + var x = [ + ...?p?.toList(), + ]; +} +''', [ + lint(46, 6), + ]); + } + + test_noToList() async { + await assertNoDiagnostics(r''' +var x = [ + ...[1, 2].whereType(), +]; +'''); + } + + test_setToList() async { + await assertDiagnostics(r''' +var x = [ + ...{1, 2}.toList(), +]; +''', [ + lint(22, 6), + ]); + } + + test_setToList_nullAwareSpread() async { + await assertDiagnostics(r''' +void f(Set? p) { + var x = [ + ...?p?.toList(), + ]; +} +''', [ + lint(45, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/unreachable_from_main_test.dart b/pkg/linter/test/rules/unreachable_from_main_test.dart new file mode 100644 index 00000000000..e15194286b4 --- /dev/null +++ b/pkg/linter/test/rules/unreachable_from_main_test.dart @@ -0,0 +1,1446 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnreachableFromMainTest); + }); +} + +@reflectiveTest +class UnreachableFromMainTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'unreachable_from_main'; + + test_class_instanceField_reachable_overrides_local() async { + await assertDiagnostics(r''' +void main() { + B(); +} + +class A { + int? f; +} + +class B extends A { + int? f; +} +''', [ + lint(41, 1), + ]); + } + + test_class_instanceField_reachable_read() async { + await assertNoDiagnostics(r''' +void main() { + C().f; +} + +class C { + int? f; +} +'''); + } + + test_class_instanceField_reachable_write() async { + await assertNoDiagnostics(r''' +void main() { + C().f = 0; +} + +class C { + int? f; +} +'''); + } + + test_class_instanceField_unreachable() async { + await assertDiagnostics(r''' +void main() { + C(); +} + +class C { + int? f; +} +''', [ + lint(41, 1), + ]); + } + + test_class_instanceGetter_reachable_invoked() async { + await assertNoDiagnostics(r''' +void main() { + C().foo; +} + +class C { + int get foo => 0; +} +'''); + } + + test_class_instanceGetter_reachable_overrides() async { + await assertDiagnostics(r''' +void main() { + B(); +} + +class A { + int get foo => 0; +} + +class B extends A { + int get foo => 0; +} +''', [ + lint(44, 3), + ]); + } + + test_class_instanceGetter_unreachable() async { + await assertDiagnostics(r''' +void main() { + C(); +} + +class C { + int get foo => 0; +} +''', [ + lint(44, 3), + ]); + } + + test_class_instanceMethod_reachable_invoked() async { + await assertNoDiagnostics(r''' +void main() { + C().foo(); +} + +class C { + void foo() {} +} +'''); + } + + test_class_instanceMethod_reachable_invoked_generic() async { + await assertNoDiagnostics(r''' +void main() { + C().foo(null); +} + +class C { + void foo(T? _) {} +} +'''); + } + + test_class_instanceMethod_reachable_overrides_imported() async { + newFile('$testPackageLibPath/a.dart', r''' +class A { + void foo() {} +} +'''); + + await assertNoDiagnostics(r''' +import 'a.dart'; + +void main() { + B(); +} + +class B extends A { + void foo() {} +} +'''); + } + + test_class_instanceMethod_reachable_overrides_local() async { + await assertDiagnostics(r''' +void main() { + B(); +} + +class A { + void foo() {} +} + +class B extends A { + void foo() {} +} +''', [ + lint(41, 3), + ]); + } + + test_class_instanceMethod_unreachable() async { + await assertDiagnostics(r''' +void main() { + C(); +} + +class C { + void foo() {} +} +''', [ + lint(41, 3), + ]); + } + + test_class_instanceSetter_reachable_invoked() async { + await assertNoDiagnostics(r''' +void main() { + C().foo = 0; +} + +class C { + set foo(int _) {} +} +'''); + } + + test_class_instanceSetter_reachable_overrides() async { + await assertDiagnostics(r''' +void main() { + B(); +} + +class A { + set foo(int _) {} +} + +class B extends A { + set foo(int _) {} +} +''', [ + lint(40, 3), + ]); + } + + test_class_instanceSetter_unreachable() async { + await assertDiagnostics(r''' +void main() { + C(); +} + +class C { + set foo(int _) {} +} +''', [ + lint(40, 3), + ]); + } + + test_class_reachable_mainInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +void main() => A() +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; + +class A {} +'''); + } + + test_class_reachable_referencedDeepInTypeAnnotation_externalMethodDeclaration() async { + await assertNoDiagnostics(r''' +void main() { + D().f; +} + +class C {} + +class D { + external C Function() f(); +} +'''); + } + + test_class_reachable_referencedDeepInTypeArgument() async { + await assertNoDiagnostics(r''' +void main() { + C(); +} + +class C {} + +class D {} +'''); + } + + test_class_reachable_referencedInTypeAnnotation_externalFieldDeclaration() async { + await assertNoDiagnostics(r''' +void main() { + D().c; +} + +class C {} + +class D { + external C? c; +} +'''); + } + + test_class_reachable_referencedInTypeAnnotation_externalMethodDeclaration() async { + await assertNoDiagnostics(r''' +void main() { + D().f; +} + +class C {} + +class D { + external C f(); +} +'''); + } + + test_class_reachable_referencedInTypeArgument() async { + await assertNoDiagnostics(r''' +void main() { + C(); +} + +class C {} + +class D {} +'''); + } + + test_class_reachableViaAnnotation() async { + await assertNoDiagnostics(r''' +void main() { + f(); +} + +class C { + const C(); +} + +@C() +void f() {} +'''); + } + + test_class_reachableViaComment() async { + await assertNoDiagnostics(r''' +/// See [C]. +void main() {} + +class C {} +'''); + } + + test_class_reachableViaDefaultValueType() async { + await assertNoDiagnostics(r''' +void main() { + f(); +} + +class C { + const C(); +} + +void f([Object? p = const C()]) {} +'''); + } + + test_class_referencedInObjectPattern() async { + await assertDiagnostics(r''' +class C {} + +void main() { + f(); +} + +void f([Object? c]) { + if (c case C()) {} +} +''', [ + lint(6, 1), + ]); + } + + test_class_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +class C {} +''', [ + lint(22, 1), + ]); + } + + test_class_unreachable_foundInAsExpression() async { + await assertDiagnostics(r''' +class A {} + +void main() { + f(); +} + +void f([Object? o]) { + o as A; +} +''', [ + lint(6, 1), + ]); + } + + test_class_unreachable_foundInAsPattern() async { + await assertDiagnostics(r''' +class A {} + +void main() { + f(); +} + +void f([(Object, )? l]) { + var (_ as A, ) = l!; +} +''', [ + lint(6, 1), + ]); + } + + test_class_unreachable_foundInIsExpression() async { + await assertDiagnostics(r''' +class A {} + +void main() { + f(); +} + +void f([Object? o]) { + o is A; +} +''', [ + lint(6, 1), + ]); + } + + test_class_unreachable_hasNamedConstructors() async { + await assertDiagnostics(r''' +void main() {} + +class C { + C(); + C.named(); +} +''', [ + lint(22, 1), + // TODO(srawlins): See if we can skip reporting a declaration if its + // enclosing declaration is being reported. + lint(28, 1), + lint(37, 5), + ]); + } + + test_class_unreachable_mainInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +void main() {} +'''); + await assertDiagnostics(r''' +part 'part.dart'; + +class A {} +''', [ + lint(25, 1), + ]); + } + + test_class_unreachable_referencedInParameter_externalMethodDeclaration() async { + await assertDiagnostics(r''' +void main() { + D().f; +} + +class C {} + +class D { + external f(C c); +} +''', [ + lint(32, 1), + ]); + } + + test_class_unreachable_referencedInTypeAnnotation_fieldDeclaration() async { + await assertDiagnostics(r''' +void main() { + D().c; +} + +class C {} + +class D { + C? c; +} +''', [ + lint(32, 1), + ]); + } + + test_class_unreachable_referencedInTypeAnnotation_parameter() async { + await assertDiagnostics(r''' +void main() { + f(); +} + +void f([C? c]) {} + +class C {} +''', [ + lint(49, 1), + ]); + } + + test_class_unreachable_referencedInTypeAnnotation_topLevelVariable() async { + await assertDiagnostics(r''' +void main() { + print(c); +} + +C? c; + +class C {} +''', [ + lint(42, 1), + ]); + } + + test_class_unreachable_referencedInTypeAnnotation_variableDeclaration() async { + await assertDiagnostics(r''' +void main() { + C? c; +} + +class C {} +''', [ + lint(31, 1), + ]); + } + + test_class_unreachable_typedefBound() async { + await assertDiagnostics(r''' +void main() { + f(); +} + +class C {} + +void f() {} +''', [ + lint(30, 1), + ]); + } + + test_classInPart_reachable() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +class A {} +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; + +void main() => A(); +'''); + } + + test_classInPart_reachable_mainInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +class A {} + +void main() => A() +'''); + await assertNoDiagnostics(r''' +part 'part.dart'; +'''); + } + + test_classInPart_unreachable() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +class A {} +'''); + await assertDiagnostics(r''' +part 'part.dart'; + +void main() {} +''', [ + lint(28, 1), + ]); + } + + test_classInPart_unreachable_mainInPart() async { + newFile('$testPackageLibPath/part.dart', r''' +part of 'test.dart'; + +class A {} + +void main() {} +'''); + await assertDiagnostics(r''' +part 'part.dart'; +''', [ + lint(28, 1), + ]); + } + + test_constructor_named_onEnum() async { + await assertDiagnostics(r''' +void main() { + E.one; + E.two; +} + +enum E { + one(), two(); + + const E(); + const E.named(); +} +''', [ + // No lint. + error(WarningCode.UNUSED_ELEMENT, 84, 5), + ]); + } + + test_constructor_named_reachableViaDirectCall() async { + await assertNoDiagnostics(r''' +void main() { + C.named(); +} + +class C { + C.named(); +} +'''); + } + + test_constructor_named_reachableViaExplicitSuperCall() async { + await assertNoDiagnostics(r''' +void main() { + D(); +} + +class C { + C.named(); +} + +class D extends C { + D() : super.named(); +} +'''); + } + + test_constructor_named_reachableViaRedirectedConstructor() async { + await assertNoDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C.named(); + factory C.two() = C.named; +} +'''); + } + + test_constructor_named_reachableViaRedirection() async { + await assertNoDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C.named(); + + C.two() : this.named(); +} +'''); + } + + test_constructor_named_reachableViaTearoff() async { + await assertNoDiagnostics(r''' +void main() { + C.named; +} + +class C { + C.named(); +} +'''); + } + + test_constructor_named_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + C.named(); +} +''', [ + lint(36, 5), + ]); + } + + test_constructor_named_unreachable_otherHasRedirectedConstructor() async { + await assertDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C.named(); + C.one(); + factory C.two() = C.one; +} +''', [ + lint(42, 5), + ]); + } + + test_constructor_reachableViaTestReflectiveLoader() async { + var testReflectiveLoaderPath = '$workspaceRootPath/test_reflective_loader'; + var packageConfigBuilder = PackageConfigFileBuilder(); + packageConfigBuilder.add( + name: 'test_reflective_loader', + rootPath: testReflectiveLoaderPath, + ); + writeTestPackageConfig(packageConfigBuilder); + newFile('$testReflectiveLoaderPath/lib/test_reflective_loader.dart', r''' +library test_reflective_loader; + +const Object reflectiveTest = _ReflectiveTest(); +class _ReflectiveTest { + const _ReflectiveTest(); +} +'''); + await assertNoDiagnostics(r''' +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + // Usually some reference via `defineReflectiveTests`. + A; +} + +@reflectiveTest +class A { + A() {} +} +'''); + } + + test_constructor_unnamed_reachableViaDefaultImplicitSuperCall() async { + await assertNoDiagnostics(r''' +void main() { + D(); +} + +class C { + C(); +} + +class D extends C { + // Just a default constructor. +} +'''); + } + + test_constructor_unnamed_reachableViaDirectCall() async { + await assertNoDiagnostics(r''' +void main() { + C(); +} + +class C { + C(); +} +'''); + } + + test_constructor_unnamed_reachableViaExplicitSuperCall() async { + await assertNoDiagnostics(r''' +void main() { + D(); +} + +class C { + C(); +} + +class D extends C { + D() : super(); +} +'''); + } + + test_constructor_unnamed_reachableViaImplicitSuperCall() async { + await assertNoDiagnostics(r''' +void main() { + D(); +} + +class C { + C(); +} + +class D extends C { + D(); +} +'''); + } + + test_constructor_unnamed_reachableViaImplicitSuperCall_indirectly() async { + await assertNoDiagnostics(r''' +void main() { + E(); +} + +class C { + C(); +} + +class D extends C { + // Just a default constructor. +} + +class E extends D { + E(); +} +'''); + } + + test_constructor_unnamed_reachableViaImplicitSuperCall_superParameters() async { + await assertNoDiagnostics(r''' +void main() { + D(1); +} + +class C { + C(int p); +} + +class D extends C { + D(super.p); +} +'''); + } + + test_constructor_unnamed_reachableViaRedirectedConstructor() async { + await assertNoDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C(); + factory C.two() = C; +} +'''); + } + + test_constructor_unnamed_reachableViaRedirection() async { + await assertNoDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C(); + + C.two() : this(); +} +'''); + } + + test_constructor_unnamed_reachableViaTearoff() async { + await assertNoDiagnostics(r''' +void main() { + C.new; +} + +class C { + C(); +} +'''); + } + + test_constructor_unnamed_referencedInConstantPattern() async { + await assertDiagnostics(r''' +class C { + const C(); +} + +void main() { + f(); +} + +void f([C? c]) { + if (c case const C()) {} +} +''', [ + lint(6, 1), + lint(18, 1), + ]); + } + + test_constructor_unnamed_referencedInConstantPattern_generic() async { + await assertDiagnostics(r''' +class C { + const C(); +} + +void main() { + f(); +} + +void f([C? c]) { + if (c case const C()) {} +} +''', [ + lint(6, 1), + lint(21, 1), + ]); + } + + test_constructor_unnamed_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + C(); +} +''', [ + lint(34, 1), + ]); + } + + test_constructor_unnamed_unreachable_otherHasRedirection() async { + await assertDiagnostics(r''' +void main() { + C.two(); +} + +class C { + C(); + C.one(); + C.two() : this.one(); +} +''', [ + lint(40, 1), + ]); + } + + test_enum_reachableViaValue() async { + await assertNoDiagnostics(r''' +void main() { + E.one; +} + +enum E { one, two } +'''); + } + + test_enum_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +enum E { one, two } +''', [ + lint(21, 1), + ]); + } + + test_extension_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +extension IntExtension on int {} +''', [ + lint(26, 12), + ]); + } + + test_mixin_reachable_implemented() async { + await assertNoDiagnostics(r''' +void main() { + A(); +} + +mixin M {} + +class A implements M {} +'''); + } + + test_mixin_reachable_mixed() async { + await assertNoDiagnostics(r''' +void main() { + A(); +} + +mixin M {} + +class A with M {} +'''); + } + + test_mixin_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +mixin M {} +''', [ + lint(22, 1), + ]); + } + + test_staticField_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + static int f = 1; +} +''', [ + lint(45, 1), + ]); + } + + test_staticFieldOnClass_reachable() async { + await assertNoDiagnostics(r''' +void main() { + C.f; +} + +class C { + static int f = 1; +} +'''); + } + + test_staticFieldOnEnum_reachable() async { + await assertNoDiagnostics(r''' +void main() { + E.f; +} + +enum E { + one, two, three; + static int f = 1; +} +'''); + } + + test_staticFieldOnExtension_reachable() async { + await assertNoDiagnostics(r''' +void main() { + E.f; +} + +extension E on int { + static int f = 1; +} +'''); + } + + test_staticFieldOnMixin_reachable() async { + await assertNoDiagnostics(r''' +void main() { + M.f; +} + +mixin M { + static int f = 1; +} +'''); + } + + test_staticGetter_reachable() async { + await assertNoDiagnostics(r''' +void main() { + C.g; +} + +class C { + static int get g => 7; +} +'''); + } + + test_staticGetter_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + static int get g => 7; +} +''', [ + lint(49, 1), + ]); + } + + test_staticMethod_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + static void f() {} +} +''', [ + lint(46, 1), + ]); + } + + test_staticMethodOnClass_reachable() async { + await assertNoDiagnostics(r''' +void main() { + C.f(); +} + +class C { + static void f() {} +} +'''); + } + + test_staticMethodOnEnum_reachable() async { + await assertNoDiagnostics(r''' +void main() { + E.f(); +} + +enum E { + one, two, three; + static void f() {} +} +'''); + } + + test_staticMethodOnExtension_reachable() async { + await assertNoDiagnostics(r''' +void main() { + E.f(); +} + +extension E on int { + static void f() {} +} +'''); + } + + test_staticMethodOnMixin_reachable() async { + await assertNoDiagnostics(r''' +void main() { + M.f(); +} + +mixin M { + static void f() {} +} +'''); + } + + test_staticSetter_reachable() async { + await assertNoDiagnostics(r''' +void main() { + C.s = 1; +} + +class C { + static set s(int value) {} +} +'''); + } + + test_staticSetter_unreachable() async { + await assertDiagnostics(r''' +void main() { + C; +} + +class C { + static set s(int value) {} +} +''', [ + lint(45, 1), + ]); + } + + test_topLevelFunction_reachable() async { + await assertNoDiagnostics(r''' +void main() { + f1(); +} + +void f1() { + f2(); +} + +void f2() {} +'''); + } + + test_topLevelFunction_reachable_private() async { + await assertNoDiagnostics(r''' +void main() { + _f(); +} + +void _f() {} +'''); + } + + test_topLevelFunction_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +void f() {} +''', [ + lint(21, 1), + ]); + } + + test_topLevelFunction_unreachable_unrelatedPragma() async { + await assertDiagnostics(r''' +void main() {} + +@pragma('other') +void f() {} +''', [ + lint(38, 1), + ]); + } + + test_topLevelFunction_unreachable_visibleForTesting() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +void main() {} + +@visibleForTesting +void f() {} +'''); + } + + test_topLevelFunction_vmEntryPoint() async { + await assertNoDiagnostics(r''' +@pragma('vm:entry-point') +void f6() {} +'''); + } + + test_topLevelFunction_vmEntryPoint_const() async { + await assertNoDiagnostics(r''' +const entryPoint = pragma('vm:entry-point'); +@entryPoint +void f6() {} +'''); + } + + test_topLevelGetter_reachable() async { + await assertNoDiagnostics(r''' +void main() { + g; +} + +int get g => 7; +'''); + } + + test_topLevelGetter_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +int get g => 7; +''', [ + lint(24, 1), + ]); + } + + test_topLevelSetter_reachable() async { + await assertNoDiagnostics(r''' +void main() { + s = 7; +} + +set s(int value) {} +'''); + } + + test_topLevelSetter_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +set s(int value) {} +''', [ + lint(20, 1), + ]); + } + + test_topLevelVariable_reachable() async { + await assertNoDiagnostics(r''' +void main() { + _f(); +} + +void _f() { + x; +} + +int x = 1; +'''); + } + + test_topLevelVariable_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +int x = 1; +''', [ + lint(20, 1), + ]); + } + + test_typedef_reachable_referencedInObjectPattern() async { + await assertNoDiagnostics(r''' +typedef T = int; + +void main() { + f(); +} + +void f([Object? c]) { + if (c case T()) {} +} +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_asExpression() async { + await assertNoDiagnostics(r''' +void main() { + 1.5 as T; +} + +typedef T = int; +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_castPattern() async { + await assertNoDiagnostics(r''' +void main() { + var r = (1.5, ); + var (s as T, ) = r; +} + +typedef T = int; +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_genericFunctionType() async { + await assertNoDiagnostics(r''' +void main() { + t = () => 7; +} + +T? Function()? t; + +typedef T = int; +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_isExpression() async { + await assertNoDiagnostics(r''' +void main() { + 1.5 is T; +} + +typedef T = int; +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_parameter() async { + await assertNoDiagnostics(r''' +void main() { + f(() {}); +} + +void f([Cb? c]) {} + +typedef Cb = void Function(); +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_recordType() async { + await assertNoDiagnostics(r''' +void main() { + t = (1, ); +} + +(T, )? t; + +typedef T = int; +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_topLevelVariable() async { + await assertNoDiagnostics(r''' +void main() { + c = () {}; +} + +Cb? c; + +typedef Cb = void Function(); +'''); + } + + test_typedef_reachable_referencedInTypeAnnotation_variableDeclaration() async { + await assertNoDiagnostics(r''' +void main() { + Cb c = () {}; +} + +typedef Cb = void Function(); +'''); + } + + test_typedef_unreachable() async { + await assertDiagnostics(r''' +void main() {} + +typedef T = String; +''', [ + lint(24, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/unrelated_type_equality_checks_test.dart b/pkg/linter/test/rules/unrelated_type_equality_checks_test.dart new file mode 100644 index 00000000000..3608f6b1f08 --- /dev/null +++ b/pkg/linter/test/rules/unrelated_type_equality_checks_test.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UnrelatedTypeEqualityChecksTest); + }); +} + +@reflectiveTest +class UnrelatedTypeEqualityChecksTest extends LintRuleTest { + @override + String get lintRule => 'unrelated_type_equality_checks'; + + test_recordAndInterfaceType_unrelated() async { + await assertDiagnostics(r''' +bool f((int, int) a, String b) => a == b; +''', [ + lint(36, 2), + ]); + } + + test_records_related() async { + await assertNoDiagnostics(r''' +bool f((int, int) a, (num, num) b) => a == b; +'''); + } + + test_records_unrelated() async { + await assertDiagnostics(r''' +bool f((int, int) a, (String, String) b) => a == b; +''', [ + lint(46, 2), + ]); + } + + test_recordsWithNamed_related() async { + await assertNoDiagnostics(r''' +bool f(({int one, int two}) a, ({num two, num one}) b) => a == b; +'''); + } + + test_recordsWithNamed_unrelated() async { + await assertDiagnostics(r''' +bool f(({int one, int two}) a, ({String one, String two}) b) => a == b; +''', [ + lint(66, 2), + ]); + } + + test_recordsWithNamedAndPositional_related() async { + await assertNoDiagnostics(r''' +bool f((int, {int two}) a, (num one, {num two}) b) => a == b; +'''); + } + + test_recordsWithNamedAndPositional_unrelated() async { + await assertDiagnostics(r''' +bool f((int, {int two}) a, (String one, {String two}) b) => a == b; +''', [ + lint(62, 2), + ]); + } + + test_switchExpression() async { + await assertDiagnostics(r''' +const space = 32; + +String f(int char) { + return switch (char) { + == 'space' => 'space', + }; +} +''', [ + error(CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION, 49, 6), + lint(69, 10), + ]); + } + + test_switchExpression_lessEq_ok() async { + await assertDiagnostics(r''' +String f(int i) { + return switch (i) { + <= 1 => 'one', + }; +} +''', [ + // No lint. + error(CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION, 27, 6) + ]); + } + + test_switchExpression_notEq() async { + await assertDiagnostics(r''' +const space = 32; + +String f(int char) { + return switch (char) { + != 'space' => 'space', + }; +} +''', [ + error(CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION, 49, 6), + lint(69, 10), + ]); + } + + test_switchExpression_ok() async { + await assertDiagnostics(r''' +String f(String char) { + return switch (char) { + == 'space' => 'space', + }; +} +''', [ + // No lint. + error(CompileTimeErrorCode.NON_EXHAUSTIVE_SWITCH_EXPRESSION, 33, 6), + ]); + } +} diff --git a/pkg/linter/test/rules/use_build_context_synchronously_test.dart b/pkg/linter/test/rules/use_build_context_synchronously_test.dart new file mode 100644 index 00000000000..fc3abb1fc86 --- /dev/null +++ b/pkg/linter/test/rules/use_build_context_synchronously_test.dart @@ -0,0 +1,2141 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/src/test_utilities/find_node.dart'; +import 'package:analyzer/src/utilities/legacy.dart'; +import 'package:linter/src/rules/use_build_context_synchronously.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(AsyncStateTest); + defineReflectiveTests(UseBuildContextSynchronouslyTest); + defineReflectiveTests(UseBuildContextSynchronouslyMixedModeTest); + }); +} + +@reflectiveTest +class AsyncStateTest extends PubPackageResolutionTest { + @override + bool get addFlutterPackageDep => true; + + FindNode get findNode => FindNode(result.content, result.unit); + + Future resolveCode(String code) async { + addTestFile(code); + await resolveTestFile(); + } + + test_adjacentStrings_referenceAfter_awaitInString() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + '' '${await Future.value()}'; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_adjacentStrings_referenceInSecondString_awaitInFirstString() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + '${await Future.value()}' '${context /* ref */}'; +} +'''); + var adjacentStrings = findNode.adjacentStrings('await'); + var reference = findNode.stringInterpolation('context /* ref */'); + expect(adjacentStrings.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_awaitExpression_referenceInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + await Future.value(context /* ref */); +} +'''); + var await_ = findNode.awaitExpression('await'); + var reference = findNode.instanceCreation('context /* ref */'); + expect(await_.asyncStateFor(reference), isNull); + } + + test_block_referenceThenAwait() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + await Future.value(); + context /* ref */; +} +'''); + var block = findNode.block('context /* ref */'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_cascade_referenceAfter_awaitInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value())..toString(); + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_cascade_referenceAfterTarget_awaitAfterTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + []..add(await Future.value())..add(context /* ref */); +} +'''); + var cascade = findNode.cascade('await'); + var reference = findNode.methodInvocation('context /* ref */'); + expect(cascade.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_cascade_referenceAfterTarget_awaitInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value())..add(context /* ref */); +} +'''); + var cascade = findNode.cascade('await'); + var reference = findNode.methodInvocation('context /* ref */'); + expect(cascade.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_cascade_referenceAfterTarget_awaitInTarget2() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value())..add(1)..add(context /* ref */); +} +'''); + var cascade = findNode.cascade('await'); + var reference = findNode.methodInvocation('context /* ref */'); + expect(cascade.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_conditional_referenceAfter_awaitInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + await Future.value(true) ? null : null; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_conditional_referenceAfter_awaitInThen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + false ? await Future.value() : null; + context /* ref */; +} +'''); + var block = findNode.block('false'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_conditional_referenceInElse_mountedCheckInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + context.mounted ? null : context /* ref */; +} +'''); + var conditional = findNode.conditionalExpression('context /* ref */'); + var reference = findNode.simple('context /* ref */'); + expect(conditional.asyncStateFor(reference), isNull); + } + + test_conditional_referenceInElse_notMountedCheckInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + !context.mounted ? null : context /* ref */; +} +'''); + var conditional = findNode.conditionalExpression('context /* ref */'); + var reference = findNode.simple('context /* ref */'); + expect( + conditional.asyncStateFor(reference), equals(AsyncState.mountedCheck)); + } + + test_conditional_referenceInThen_mountedCheckInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + context.mounted ? context /* ref */ : null; +} +'''); + var conditional = findNode.conditionalExpression('context /* ref */'); + var reference = findNode.simple('context /* ref */'); + expect( + conditional.asyncStateFor(reference), equals(AsyncState.mountedCheck)); + } + + test_conditional_referenceInThen_notMountedCheckInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + !context.mounted ? context /* ref */ : null; +} +'''); + var conditional = findNode.conditionalExpression('context /* ref */'); + var reference = findNode.simple('context /* ref */'); + expect(conditional.asyncStateFor(reference), isNull); + } + + test_doWhileStatement_referenceInBody_asyncInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + do { + await Future.value(); + context /* ref */; + } while (true); +} +'''); + var block = findNode.block('context /* ref */'); + var reference = findNode.statement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_doWhileStatement_referenceInBody_asyncInBody2() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + do { + context /* ref */; + await Future.value(); + } while (true); +} +'''); + var block = findNode.block('context /* ref */'); + var reference = findNode.statement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_extensionOverride_referenceAfter_awaitInArgument() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + E(await Future.value(false)).f(); + context /* ref */; +} + +extension E on int { + void f() {} +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithDeclaration_referenceAfter_awaitInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var i = 0; i < await Future.value(1); i++) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithDeclaration_referenceAfter_awaitInPartInitialization() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var i = await Future.value(1); i < 7; i++) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithDeclaration_referenceAfter_awaitInPartUpdaters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var i = 0; i < 5; i += await Future.value()) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithDeclaration_referenceInExpression_mountedCheckInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var i = 0; context.mounted; i++) context /* ref */, + ]; +} +'''); + var forElement = findNode.forElement('for '); + var reference = findNode.expression('context /* ref */'); + expect(forElement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_forElementWithEach_referenceAfter_awaitInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var e in []) await Future.value(), + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithEach_referenceAfter_awaitInPartExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + for (var e in await Future.value([])) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithExpression_referenceAfter_awaitInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + [ + for (i = 0; i < await Future.value(1); i++) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithExpression_referenceAfter_awaitInPartInitialization() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + [ + for (i = await Future.value(1); i < 7; i++) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithExpression_referenceAfter_awaitInPartUpdaters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + [ + for (i = 0; i < 5; i += await Future.value()) null, + ]; + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forElementWithExpression_referenceAfter_mountedCheckInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + [ + for (i = 0; context.mounted; i++) context /* ref */, + ]; +} +'''); + var forElement = findNode.forElement('for '); + var reference = findNode.expression('context /* ref */'); + expect(forElement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_forStatement_referenceInBody_asyncInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var a in []) { + await Future.value(); + context /* ref */; + } +} +'''); + var block = findNode.block('context /* ref */'); + var reference = findNode.statement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatement_referenceInBody_asyncInBody2() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var a in []) { + context /* ref */; + await Future.value(); + } +} +'''); + var block = findNode.block('context /* ref */'); + var reference = findNode.statement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithDeclaration_referenceAfter_awaitInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var i = 0; i < await Future.value(1); i++) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithDeclaration_referenceAfter_awaitInPartInitialization() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var i = await Future.value(1); i < 7; i++) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithDeclaration_referenceAfter_awaitInPartUpdaters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var i = 0; i < 5; i += await Future.value()) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithDeclaration_referenceInBody_mountedCheckInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var i = 0; context.mounted; i++) context /* ref */; +} +'''); + var forStatement = findNode.forStatement('for '); + var reference = findNode.expressionStatement('context /* ref */'); + expect(forStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_forStatementWithEach_referenceAfter_awaitInPartExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + for (var e in await Future.value([])) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithExpression_referenceAfter_awaitInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + for (i = 0; i < await Future.value(1); i++) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithExpression_referenceAfter_awaitInPartInitialization() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + for (i = await Future.value(1); i < 7; i++) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithExpression_referenceAfter_awaitInPartUpdaters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + for (i = 0; i < 5; i += await Future.value()) null; + context /* ref */; +} +'''); + var block = findNode.forStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_forStatementWithExpression_referenceAfter_mountedCheckInPartCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, int i) async { + for (i = 0; context.mounted; i++) context /* ref */; +} +'''); + var forStatement = findNode.forStatement('for '); + var reference = findNode.expressionStatement('context /* ref */'); + expect(forStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_functionExpressionInvocation_referenceAfter_awaitInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + ((await Future.value()).add)(1); + context /* ref */; +} +'''); + var block = findNode.block('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifElement_referenceAfter_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + if (await Future.value(true)) null, + context /* ref */, + ]; +} +'''); + var ifElement = findNode.listLiteral('if ('); + var reference = findNode.expression('context /* ref */,'); + expect(ifElement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifElement_referenceInElse_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + if (await Future.value(true)) null else context /* ref */, + ]; +} +'''); + var ifElement = findNode.ifElement('if ('); + var reference = findNode.expression('context /* ref */'); + expect(ifElement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifElement_referenceInElse_mountedGuardInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + if (context.mounted) null else context /* ref */, + ]; +} +'''); + var ifElement = findNode.ifElement('if ('); + var reference = findNode.expression('context /* ref */'); + expect(ifElement.asyncStateFor(reference), isNull); + } + + test_ifElement_referenceInElse_notMountedGuardInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + if (!context.mounted) null else context /* ref */, + ]; +} +'''); + var ifElement = findNode.ifElement('if ('); + var reference = findNode.expression('context /* ref */'); + expect(ifElement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifElement_referenceInThen_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [ + if (await Future.value(true)) context /* ref */, + ]; +} +'''); + var ifElement = findNode.ifElement('if ('); + var reference = findNode.expression('context /* ref */'); + expect(ifElement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceAfter_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (await Future.value(true)) { + return; + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if (').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceAfter_asyncInThen_notMountedGuardInElse() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2) { + await Future.value(); + } else { + if (!mounted) return; + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if (1').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceAfter_awaitThenExitInElse() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2) { + } else { + await Future.value(); + return; + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if ').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), isNull); + } + + test_ifStatement_referenceAfter_awaitThenExitInThen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2) { + await Future.value(); + return; + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if ').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), isNull); + } + + test_ifStatement_referenceAfter_notMountedCheckInCondition_break() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case (1): + if (!context.mounted) break; + context /* ref */; + } +} +'''); + var block = findNode.ifStatement('if ').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.notMountedCheck); + } + + test_ifStatement_referenceAfter_notMountedCheckInCondition_exit() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (!context.mounted) return; + context /* ref */; +} +'''); + var block = findNode.ifStatement('if ').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.notMountedCheck); + } + + test_ifStatement_referenceAfter_notMountedGuardInCondition_exitInThen_awaitInElse() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (!context.mounted) { + return; + } else { + await f(); + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if (').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceAfter_notMountedGuardsInThenAndElse() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2) { + if (!mounted) return; + } else { + if (!mounted) return; + } + context /* ref */; +} +'''); + var block = findNode.ifStatement('if (1').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.notMountedCheck); + } + + test_ifStatement_referenceInElse_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (await Future.value(true)) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if ('); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceInElse_mountedGuardInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (context.mounted) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if ('); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), isNull); + } + + test_ifStatement_referenceInElse_notMounted() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (!context.mounted) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInElse_notMountedOrUninterestingInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (!context.mounted || 1 == 2) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInElse_uninterestingAndNotMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2 && !context.mounted) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInElse_uninterestingOrNotMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2 || !context.mounted) { + } else { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), isNull); + } + + test_ifStatement_referenceInThen_asyncInAssignmentInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, bool m) async { + if (m = context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if ('); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInThen_asyncInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (await Future.value(true)) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if ('); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceInThen_awaitAndMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (await Future.value(true) && context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInThen_awaitOrMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (await Future.value(true) || context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceInThen_conditionAndMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2 && context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInThen_mountedAndAwaitInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (context.mounted && await Future.value(true)) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceInThen_mountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_ifStatement_referenceInThen_mountedOrAwaitInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (context.mounted || await Future.value(true)) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_ifStatement_referenceInThen_uninterestingOrMountedInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + if (1 == 2 || context.mounted) { + context /* ref */; + } +} +'''); + var ifStatement = findNode.ifStatement('if '); + var reference = findNode.block('context /* ref */'); + expect(ifStatement.asyncStateFor(reference), isNull); + } + + test_indexExpression_referenceInRhs_asyncInIndex() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, List list) async { + list[await c()] = context /* ref */; +} +'''); + var indexExpression = findNode.assignment('['); + var reference = findNode.expression('context /* ref */'); + expect(indexExpression.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_instanceCreationExpression_referenceInParameters_awaitInParameters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + new List.filled(await Future.value(1), context /* ref */); +} +'''); + var instanceCreation = findNode.instanceCreation('await'); + var reference = findNode.argumentList('context /* ref */'); + expect(instanceCreation.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_isExpression_referenceAfter_awaitInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + await Future.value() is int; + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_methodInvocation_referenceAfter_asyncInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value([])).add(1); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_methodInvocation_referenceAfter_awaitInParameters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [].indexOf(1, await Future.value()); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_methodInvocation_referenceAfter_awaitInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value()).add(1); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_methodInvocation_referenceInParameters_awaitInParameters() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + f(await c(), context /* ref */); +} +void f(_, _) {} +'''); + var methodInvocation = findNode.methodInvocation('await'); + var reference = findNode.argumentList('context /* ref */'); + expect(methodInvocation.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_postfix_referenceAfter_awaitInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value())!; + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_propertyAccess_referenceAfter_awaitInTarget() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value(true)).isEven; + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_recordLiteral_referenceAfter_awaitInField() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (await Future.value(), ); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_recordLiteral_referenceAfter_awaitInNamedField() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (f: await Future.value(), ); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_spread_referenceAfter_awaitInSpread() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + [...(await Future.value())]; + context /* ref */; +} +'''); + var block = findNode.expressionStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_stringInterpolation_referenceAfter_awaitInStringInterpolation() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + '${await Future.value()}'; + context /* ref */; +} +'''); + var block = findNode.stringInterpolation('await').parent!.parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchExpression_referenceAfter_awaitInCaseBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (switch (1) { + _ => await c(), + }); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('switch').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchExpression_referenceAfter_awaitInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (switch (await Future.value()) { + _ => null, + }); + context /* ref */; +} +'''); + var block = findNode.expressionStatement('switch').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchExpression_referenceInExpression_awaitInCondition() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (switch (await Future.value()) { + _ => context /* ref */, + }); +} +'''); + var switchExpression = findNode.switchExpression('switch'); + var reference = findNode.switchExpressionCase('context /* ref */'); + expect(switchExpression.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchExpression_referenceInExpression_awaitInWhenClause() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (switch (1) { + _ when await Future.value(true) => context /* ref */, + _ => null, + }); +} +'''); + var switchExpression = findNode.switchExpressionCase('await'); + var reference = findNode.simple('context /* ref */'); + expect(switchExpression.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchExpression_referenceInExpression_mountedGuardInWhenClause() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + (switch (1) { + _ when context.mounted => context /* ref */, + _ => null, + }); +} +'''); + var switchExpression = findNode.switchExpressionCase('when'); + var reference = findNode.simple('context /* ref */'); + expect(switchExpression.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_switchExpression_referenceInWhenClause_awaitInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context, bool Function(BuildContext) condition) async { + (switch (1) { + _ when condition(context /* ref */) => await Future.value(), + _ => null, + }); +} +'''); + var switchExpression = findNode.switchExpressionCase('await'); + var reference = findNode.whenClause('context /* ref */').parent!; + expect(switchExpression.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceAfter_awaitInCaseBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1: + await Future.value(); + } + context /* ref */; +} +'''); + var block = findNode.switchStatement('switch').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceAfter_awaitInCaseWhen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when await Future.value(): + } + context /* ref */; +} +'''); + var block = findNode.switchStatement('case').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceAfter_awaitInDefault() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + default: + await Future.value(); + } + context /* ref */; +} +'''); + var block = findNode.switchStatement('switch').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceAfter_mountedCheckInCaseWhen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when context.mounted: + } + context /* ref */; +} +'''); + var block = findNode.switchStatement('case').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceInCaseBody_awaitInCaseWhen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when await Future.value(): + context /* ref */; + } +} +'''); + var switchStatement = findNode.switchStatement('switch'); + var reference = findNode.switchPatternCase('context /* ref */'); + expect(switchStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceInCaseBody_awaitInOtherCase() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1: + await Future.value(); + case 2: + context /* ref */; + } +} +'''); + var switchCase = findNode.switchStatement('switch'); + var reference = findNode.switchPatternCase('context /* ref */'); + expect(switchCase.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceInCaseBody_mountedCheckInCaseWhen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when context.mounted: + context /* ref */; + } +} +'''); + var switchStatement = findNode.switchStatement('switch'); + var reference = findNode.switchPatternCase('context /* ref */'); + expect(switchStatement.asyncStateFor(reference), AsyncState.mountedCheck); + } + + test_switchStatement_referenceInCaseBody_mountedCheckInCaseWhen2() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when true: + case 2 when context.mounted: + context /* ref */; + } +} +'''); + var switchCase = findNode.switchPatternCase('case 2'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(switchCase.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceInCaseBody_mountedCheckInCaseWhen3() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when await Future.value(true): + case 2 when context.mounted: + context /* ref */; + } +} +'''); + var switchStatement = findNode.switchStatement('switch'); + var reference = findNode.switchPatternCase('context /* ref */'); + expect(switchStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceInCaseBody_mountedCheckInCaseWhen4() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when context.mounted: + print(1); + case 2: + context /* ref */; + } +} +'''); + var switchCase = findNode.switchPatternCase('case 2'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(switchCase.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceInCaseWhen() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + case 1 when context /* ref */: + await Future.value(); + } +} +'''); + var switchCase = findNode.switchPatternCase('case'); + var reference = findNode.whenClause('context /* ref */').parent!; + expect(switchCase.asyncStateFor(reference), isNull); + } + + test_switchStatement_referenceInDefault_awaitInDefault() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + default: + await Future.value(); + context /* ref */; + } +} +'''); + var switchStatement = findNode.switchDefault('await'); + var reference = findNode.expressionStatement('context /* ref */'); + expect(switchStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_switchStatement_referenceInDefault_mountedGuardInDefault() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + switch (1) { + default: + if (!context.mounted) return; + context /* ref */; + } +} +'''); + var switchStatement = findNode.switchDefault('context.mounted'); + var reference = findNode.expressionStatement('context /* ref */'); + expect( + switchStatement.asyncStateFor(reference), AsyncState.notMountedCheck); + } + + test_tryStatement_referenceAfter_awaitInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + await Future.value(); + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_tryStatement_referenceAfter_awaitInCatch() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + } on Exception { + await Future.value(); + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_tryStatement_referenceAfter_awaitInFinally() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + } finally { + await Future.value(); + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_tryStatement_referenceAfter_notMountedCheckInCatch() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + } on Exception { + if (!context.mounted) return; + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), isNull); + } + + test_tryStatement_referenceAfter_notMountedCheckInTry() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + if (!context.mounted) return; + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), isNull); + } + + test_tryStatement_referenceAfter_notMountedGuardInFinally() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + } finally { + if (!context.mounted) return; + } + context /* ref */; +} +Future c() async {} +'''); + var block = findNode.tryStatement('try').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.notMountedCheck); + } + + test_tryStatement_referenceInCatch_awaitInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + await Future.value(); + } on Exception { + context /* ref */; + } +} +Future c() async {} +'''); + var tryStatement = findNode.tryStatement('try'); + var reference = findNode.catchClause('context /* ref */'); + expect(tryStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_tryStatement_referenceInFinally_awaitInCatch() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + try { + } on Exception { + await Future.value(); + } finally { + context /* ref */; + } +} +Future c() async {} +'''); + var tryStatement = findNode.tryStatement('try'); + var reference = findNode.block('context /* ref */'); + expect(tryStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_whileStatement_referenceAfter_asyncInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + while (true) { + await Future.value(); + break; + } + context /* ref */; +} +'''); + var block = findNode.whileStatement('while (').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_whileStatement_referenceInBody_asyncInBody() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + while (true) { + await Future.value(); + context /* ref */; + } +} +'''); + var whileStatement = findNode.whileStatement('while ('); + var reference = findNode.block('context /* ref */'); + expect(whileStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_whileStatement_referenceInBody_asyncInBody2() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + while (true) { + context /* ref */; + await Future.value(); + } +} +'''); + var whileStatement = findNode.whileStatement('while ('); + var reference = findNode.block('context /* ref */'); + expect(whileStatement.asyncStateFor(reference), AsyncState.asynchronous); + } + + test_yield_referenceAfter_asyncInExpression() async { + await resolveCode(r''' +import 'package:flutter/widgets.dart'; +void foo(BuildContext context) async { + yield (await Future.value()); + context /* ref */; +} +'''); + var block = findNode.yieldStatement('await').parent!; + var reference = findNode.expressionStatement('context /* ref */'); + expect(block.asyncStateFor(reference), AsyncState.asynchronous); + } +} + +@reflectiveTest +class UseBuildContextSynchronouslyMixedModeTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'use_build_context_synchronously'; + + /// Ensure we're not run in the test dir. + @override + String get testPackageRootPath => '$workspaceRootPath/lib'; + + @override + setUp() { + super.setUp(); + noSoundNullSafety = false; + } + + tearDown() { + noSoundNullSafety = true; + } + + /// https://github.com/dart-lang/linter/issues/2572 + test_mixedMode() async { + newFile('$testPackageLibPath/migrated.dart', ''' +import 'package:flutter/widgets.dart'; + +BuildContext? get contextOrNull => null; + +void f(BuildContext? contextOrNull) {} +'''); + + await assertNoDiagnostics(r''' +// @dart=2.9 + +import 'migrated.dart'; + +void nullableContext() async { + f(contextOrNull); + await Future.delayed(Duration()); + f(contextOrNull); +} +'''); + } +} + +@reflectiveTest +class UseBuildContextSynchronouslyTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'use_build_context_synchronously'; + + /// Ensure we're not run in the test dir. + @override + String get testPackageRootPath => '$workspaceRootPath/lib'; + + test_assignmentExpressionContainsMountedCheck_thenReferenceToContext() async { + // Assignment statement-expression with mounted check, then use of + // BuildContext in if-then statement, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await c(); + var m = context.mounted; + Navigator.of(context); +} + +Future c() async {} +''', [ + lint(121, 21), + ]); + } + + test_await_afterReferenceToContext() async { + // Use of BuildContext, then await, in statement block is OK. + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + Navigator.of(context); + await f(); +} + +Future f() async {} +'''); + } + + test_await_beforeReferenceToContext() async { + // Await, then use of BuildContext, in statement block is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await f(); + Navigator.of(context); +} + +Future f() async {} +''', [ + lint(94, 21), + ]); + } + + test_await_beforeReferenceToContext_inParens() async { + // Await, then use of BuildContext in parentheses, in statement block is + // REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await f(); + Navigator.of((context)); +} + +Future f() async {} +''', [ + lint(94, 23), + ]); + } + + test_await_beforeReferenceToContext_nullAsserted() async { + // Await, then use of null-asserted BuildContext, in statement block is + // REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext? context) async { + await f(); + Navigator.of(context!); +} + +Future f() async {} +''', [ + lint(95, 22), + ]); + } + + test_awaitBeforeForBody_referenceToContext_thenMountedGuard() async { + // Await, then for-each statement, and inside the for-body: use of + // BuildContext, then mounted guard, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await f(); + for (var e in []) { + Navigator.of(context); + if (context.mounted) return; + } +} + +Future f() async {} +''', [ + lint(118, 21), + ]); + } + + test_awaitBeforeIfStatement_withReferenceToContext() async { + // Await, then use of BuildContext in an unrelated if-body, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + var b = await c(); + if (b) { + Navigator.of(context); + } +} +Future c() async => true; +''', [ + lint(115, 21), + ]); + } + + test_awaitBeforeReferenceToContext_inClosure() async { + // Await, then use of BuildContext in a closure, is REPORTED. + // todo (pq): what about closures? + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await c(); + f1(() { + f2(context); + }); +} + +void f1(Function f) {} +void f2(BuildContext c) {} +Future c() async => true; +'''); + } + + test_awaitBeforeWhileBody_referenceToContext_thenMountedGuard() async { + // Await, then While-true statement, and inside the while-body: use of + // BuildContext, then mounted guard, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await f(); + while (true) { + Navigator.of(context); + if (context.mounted) return; + } +} + +Future f() async {} +''', [ + lint(113, 21), + ]); + } + + test_awaitInIfCondition_expressionContainsReferenceToContext() async { + // Await expression contains use of BuildContext, is OK. + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo( + BuildContext context, Future Function(BuildContext) condition) async { + if (await condition(context)) { + return; + } +} +'''); + } + + test_awaitInIfThenAndExitInElse_beforeReferenceToContext() async { + // Await in an if-body and await-and-exit in the associated else, then use + // of BuildContext, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + if (1 == 2) { + await c(); + } else { + await c(); + return; + } + Navigator.of(context); +} +Future c() async => true; +''', [ + lint(154, 21), + ]); + } + + /// https://github.com/dart-lang/linter/issues/3818 + test_context_propertyAccess() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class W { + final BuildContext context; + W(this.context); + + Future f() async { + await Future.value(); + g(this.context); + } + + Future g(BuildContext context) async {} +} +''', [ + lint(157, 15), + ]); + } + + /// https://github.com/dart-lang/linter/issues/3676 + test_contextPassedAsNamedParam() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Future foo(BuildContext context) async { + await Future.value(); + bar(context: context); +} + +Future bar({required BuildContext context}) async {} +''', [ + lint(117, 21), + ]); + } + + test_ifConditionContainsMountedAndReferenceToContext() async { + // Binary expression contains mounted check AND use of BuildContext, is + // OK. + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo( + BuildContext context, bool Function(BuildContext) condition) async { + await c(); + if (context.mounted && condition(context)) { + return; + } +} + +Future c() async {} +'''); + } + + test_ifConditionContainsMountedCheckInAssignmentLhs_thenReferenceToContext() async { + // If-condition contains assignment with mounted check on LHS, then use of + // BuildContext in if-then statement, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + await c(); + if (A(context.mounted).b = false) { + Navigator.of(context); + } +} + +class A { + bool b; + A(this.b); +} + +Future c() async {} +''', [ + lint(134, 21), + ]); + } + + test_ifConditionContainsMountedOrReferenceToContext() async { + // Binary expression contains mounted check OR use of BuildContext, is + // REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo( + BuildContext context, bool Function(BuildContext) condition) async { + await c(); + if (context.mounted || condition(context)) { + return; + } +} + +Future c() async {} +''', [ + lint(161, 18), + ]); + } + + test_ifConditionContainsNotMountedAndReferenceToContext() async { + // Binary expression contains not-mounted check AND use of BuildContext, is + // REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo( + BuildContext context, bool Function(BuildContext) condition) async { + await c(); + if (!context.mounted && condition(context)) { + return; + } +} + +Future c() async {} +''', [ + lint(162, 18), + ]); + } + + test_noAwaitBefore_ifEmptyThen_methodInvocation() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void f(BuildContext context) async { + if (true) {} + context.foo(); +} + +extension on BuildContext { + void foo() {} +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/3700 + test_propertyAccess_getter() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +extension on BuildContext { + BuildContext get foo => this; +} + +Future f(BuildContext context) async { + await Future.value(); + context.foo; +} +''', [ + lint(174, 11), + ]); + } + + test_propertyAccess_setter() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +extension on BuildContext { + set foo(int x){ } +} + +Future f(BuildContext context) async { + await Future.value(); + context.foo = 1; +} +''', [ + lint(162, 11), + ]); + } + + test_referenceToContextInAwait() async { + // An assignment expression, with an await, and use of BuildContext inside + // the await expression, is OK. + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + final x = await Future.value(context); +} + +'''); + } + + test_referenceToContextInDoWhileBody_thenAwait() async { + // Do-while statement, and inside the do-while-body: use of BuildContext, + // then await, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + do { + Navigator.of(context); + await f(); + } while (true); +} + +Future f() async {} +''', [ + lint(90, 21), + ]); + } + + test_referenceToContextInForBody_thenAwait() async { + // For-each statement, and inside the for-body: use of BuildContext, then + // await, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + for (var e in []) { + Navigator.of(context); + await f(); + } +} + +Future f() async {} +''', [ + lint(105, 21), + ]); + } + + test_referenceToContextInWhileBody_thenAwait() async { + // While statement, and inside the while-body: use of BuildContext, then + // await, is REPORTED. + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +void foo(BuildContext context) async { + while (true) { + Navigator.of(context); + await f(); + } +} + +Future f() async {} +''', [ + lint(100, 21), + ]); + } +} + +extension on AstNode { + AsyncState? asyncStateFor(AstNode reference) { + assert( + () { + if (reference.parent == this) return true; + return false; + }(), + "'reference' ($reference) (a ${reference.runtimeType}) (parent: " + '${reference.parent.runtimeType}) must be a ' + "direct child of 'this' ($this) (a $runtimeType), or a sibling in a " + 'list of AstNodes', + ); + var asyncStateTracker = AsyncStateTracker(); + return asyncStateTracker.asyncStateFor(reference); + } +} diff --git a/pkg/linter/test/rules/use_colored_box_test.dart b/pkg/linter/test/rules/use_colored_box_test.dart new file mode 100644 index 00000000000..77d31f60520 --- /dev/null +++ b/pkg/linter/test/rules/use_colored_box_test.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseColoredBoxTest); + }); +} + +@reflectiveTest +class UseColoredBoxTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'use_colored_box'; + + test_childArgument() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget containerWithChild() { + return Container( + child: SizedBox(), + ); +} +'''); + } + + test_colorArgument() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + color: Color(0xffffffff), + ); +} +'''); + } + + test_colorArgument_andChild() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget containerWithColorAndChild() { + return Container( + color: Color(0xffffffff), + child: SizedBox(), + ); +} +''', [ + lint(87, 9), + ]); + } + + test_colorArgument_named_moreArguments() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container( + color: Color(0xffffffff), + width: 20, + ); +} +'''); + } + + test_colorArgument_nullableExpression() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f(Color? myColor) { + return Container(color: myColor); +} +'''); + } + + test_keyArgument() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container(key: Key('abc')); +} +'''); + } + + test_noArgument() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +Widget f() { + return Container(); +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_enums_test.dart b/pkg/linter/test/rules/use_enums_test.dart new file mode 100644 index 00000000000..755a1520989 --- /dev/null +++ b/pkg/linter/test/rules/use_enums_test.dart @@ -0,0 +1,386 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseEnumsTest); + }); +} + +@reflectiveTest +class UseEnumsTest extends LintRuleTest { + @override + String get lintRule => 'use_enums'; + + test_constructor_private() async { + await assertDiagnostics(r''' +class A { + static const A a = A._(1); + static const A b = A._(2); + final int value; + const A._(this.value); +} +''', [ + lint(6, 1), + ]); + } + + test_extendsObject() async { + await assertDiagnostics(r''' +class A extends Object { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +''', [ + lint(6, 1), + ]); + } + + test_multiDeclaration() async { + await assertDiagnostics(r''' +class A { + static const A a = A._(), b = A._(); + const A._(); +} +''', [ + lint(6, 1), + ]); + } + + test_no_lint_abstract() async { + await assertNoDiagnostics(r''' +abstract class A { + static const A a = B(); + static const A b = B(); + const A(); +} +class B extends A { + const B(); +} +'''); + } + + test_no_lint_constructor_factory() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A(); + static const A b = A(); + factory A.f(int index) { + throw '!'; + } + const A(); +} +'''); + } + + test_no_lint_constructor_named() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A.x(); + static const A b = A.x(); + const A.x(); +} +'''); + } + + test_no_lint_constructor_public() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A(1); + static const A b = A(2); + final int index; + const A(this.index); +} +'''); + } + + test_no_lint_constructorUsedInConstructor() async { + await assertDiagnostics(r''' +class _E { + static const _E a = _E(); + static const _E b = _E(); + + const _E({_E e = const _E()}); +} +''', [ + // No lint. + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 29, 1), + error(WarningCode.UNUSED_FIELD, 57, 1), + // We are reversing the deprecation: This code will remain a `HintCode`. + // ignore: deprecated_member_use + error(HintCode.UNUSED_ELEMENT_PARAMETER, 83, 1), + ]); + } + + test_no_lint_constructorUsedOutsideClass() async { + await assertDiagnostics(r''' +class _E { + static const _E a = _E(); + static const _E b = _E(); + + const _E(); +} + +_E get e => _E(); +''', [ + // No lint. + error(WarningCode.UNUSED_FIELD, 29, 1), + error(WarningCode.UNUSED_FIELD, 57, 1), + ]); + } + + test_no_lint_extended() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +class B extends A { + const B() : super._(); +} +'''); + } + + test_no_lint_extends_notObject() async { + await assertNoDiagnostics(r''' +class O { + const O(); +} +class A extends O { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +'''); + } + + test_no_lint_factoryAll() async { + await assertDiagnostics(r''' +class _E { + static _E c = _E(); + static _E d = _E(); + + factory _E() => c; +} +''', [ + // No lint. + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 45, 1), + ]); + } + + test_no_lint_factorySome() async { + await assertDiagnostics(r''' +class _E { + static _E c0 = _E._(); + static _E c1 = _E(); + + factory _E() => c0; + const _E._(); +} +''', [ + // No lint. + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 48, 2), + ]); + } + + test_no_lint_implemented() async { + await assertDiagnostics(''' +class _E { + static const _E c = _E(); + static const _E d = _E(); + + const _E(); +} +class F implements _E {} +''', [ + // No lint. + error(WarningCode.UNUSED_FIELD, 29, 1), + error(WarningCode.UNUSED_FIELD, 57, 1), + ]); + } + + test_no_lint_implements_index_field() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); + final int index = 0; +} +'''); + } + + test_no_lint_implements_index_getter() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); + int get index => 0; +} +'''); + } + + test_no_lint_implements_values_field() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); + static final List values = [a, b]; +} +'''); + } + + test_no_lint_implements_values_method() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); + static List values() => []; +} +'''); + } + + test_no_lint_nonConstConstructor() async { + await assertDiagnostics(''' +class _E { + static final _E a = _E(); + static final _E b = _E(); + + _E(); +} +''', [ + // No lint. + // todo(pq):consider relaxing the lint to flag cases w/o a const + // but all final fields. + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 29, 1), + error(WarningCode.UNUSED_FIELD, 57, 1), + ]); + } + + test_no_lint_nonInstanceCreationInitialization() async { + await assertDiagnostics(r''' +class _E { + static const _E a = _E(); + static const _E b = a; + + const _E(); +} +''', [ + // No lint. + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 57, 1), + ]); + } + + test_no_lint_overrides_equals() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A(); + static const A b = A(); + const A(); + @override + bool operator ==(Object other) => false; +} +'''); + } + + test_no_lint_overrides_hashCode() async { + await assertNoDiagnostics(r''' +class A { + static const A a = A(); + static const A b = A(); + const A(); + @override + int get hashCode => 0; +} +'''); + } + + test_referencedFactoryConstructor() async { + await assertDiagnostics(r''' +class _E { + static const _E c = _E(); + static const _E d = _E(); + + const _E(); + + factory _E.withValue(int x) => c; +} + +_E e = _E.withValue(0); +''', [ + lint(6, 2), + error(WarningCode.UNUSED_FIELD, 57, 1), + ]); + } + + test_simple_hasPart() async { + newFile2('$testPackageLibPath/a.dart', ''' +part of 'test.dart'; +'''); + await assertDiagnostics(r''' +part 'a.dart'; +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +''', [ + lint(21, 1), + ]); + } + + test_simple_private() async { + await assertDiagnostics(r''' +class _A { + static const _A a = _A(); + static const _A b = _A(); + const _A(); +} +''', [ + error(WarningCode.UNUSED_ELEMENT, 6, 2), + error(WarningCode.UNUSED_FIELD, 29, 1), + error(WarningCode.UNUSED_FIELD, 57, 1), + lint(6, 2), + ]); + } + + test_simple_public() async { + await assertDiagnostics(r''' +class A { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +''', [ + lint(6, 1), + ]); + } + + test_withMixin() async { + await assertDiagnostics(r''' +mixin class M { } +class A with M { + static const A a = A._(); + static const A b = A._(); + const A._(); +} +''', [ + lint(28, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/use_full_hex_values_for_flutter_colors_test.dart b/pkg/linter/test/rules/use_full_hex_values_for_flutter_colors_test.dart new file mode 100644 index 00000000000..5caedfc142f --- /dev/null +++ b/pkg/linter/test/rules/use_full_hex_values_for_flutter_colors_test.dart @@ -0,0 +1,109 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseFullHexValuesForFlutterColorsTest); + }); +} + +@reflectiveTest +class UseFullHexValuesForFlutterColorsTest extends LintRuleTest { + @override + String get lintRule => 'use_full_hex_values_for_flutter_colors'; + + test_dynamicArgument() async { + await assertNoDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f(dynamic a) { + Color(a); +} +'''); + } + + test_eightDigitHex_lower() async { + await assertNoDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f() { + Color(0x00000000); +} +'''); + } + + test_eightDigitHex_upper() async { + await assertNoDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f() { + Color(0X00000000); +} +'''); + } + + test_nonHex() async { + await assertDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f() { + Color(1); +} +''', [ + lint(70, 1), + ]); + } + + test_sixDigitHex_lower() async { + await assertDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f() { + Color(0x000000); +} +''', [ + lint(70, 8), + ]); + } + + test_sixDigitHex_upper() async { + await assertDiagnostics(r''' +library dart.ui; + +class Color { + Color(int v); +} + +void f() { + Color(0X000000); +} +''', [ + lint(70, 8), + ]); + } +} diff --git a/pkg/linter/test/rules/use_function_type_syntax_for_parameters_test.dart b/pkg/linter/test/rules/use_function_type_syntax_for_parameters_test.dart new file mode 100644 index 00000000000..022740f95be --- /dev/null +++ b/pkg/linter/test/rules/use_function_type_syntax_for_parameters_test.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseFunctionTypeSyntaxForParametersTest); + }); +} + +@reflectiveTest +class UseFunctionTypeSyntaxForParametersTest extends LintRuleTest { + @override + String get lintRule => 'use_function_type_syntax_for_parameters'; + + test_classicSyntax() async { + await assertDiagnostics(r''' +void f1(bool f(int e)) {} +''', [ + lint(8, 13), + ]); + } + + test_functionTypeSyntax() async { + await assertNoDiagnostics(r''' +void f2(bool Function(int e) f) {} +'''); + } +} diff --git a/pkg/linter/test/rules/use_if_null_to_convert_nulls_to_bools_test.dart b/pkg/linter/test/rules/use_if_null_to_convert_nulls_to_bools_test.dart new file mode 100644 index 00000000000..55da524ff73 --- /dev/null +++ b/pkg/linter/test/rules/use_if_null_to_convert_nulls_to_bools_test.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseIfNullToConvertNullsToBoolsTest); + }); +} + +@reflectiveTest +class UseIfNullToConvertNullsToBoolsTest extends LintRuleTest { + @override + String get lintRule => 'use_if_null_to_convert_nulls_to_bools'; + + test_equalEqual_false() async { + await assertNoDiagnostics(r''' +bool? e; +bool r = e == false; +'''); + } + + test_equalEqual_true() async { + await assertDiagnostics(r''' +bool? e; +bool r = e == true; +''', [ + lint(18, 9), + ]); + } + + test_notEqual_false() async { + await assertDiagnostics(r''' +bool? e; +bool r = e != false; +''', [ + lint(18, 10), + ]); + } + + test_notEqual_true() async { + await assertNoDiagnostics(r''' +bool? e; +bool r = e != true; +'''); + } + + test_questionQuestion_false() async { + await assertNoDiagnostics(r''' +bool? e; +bool r = e ?? false; +'''); + } + + test_questionQuestion_true() async { + await assertNoDiagnostics(r''' +bool? e; +bool r = e ?? true; +'''); + } +} diff --git a/pkg/linter/test/rules/use_is_even_rather_than_modulo_test.dart b/pkg/linter/test/rules/use_is_even_rather_than_modulo_test.dart new file mode 100644 index 00000000000..318404389fe --- /dev/null +++ b/pkg/linter/test/rules/use_is_even_rather_than_modulo_test.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseIsEvenRatherThanModuloTest); + }); +} + +@reflectiveTest +class UseIsEvenRatherThanModuloTest extends LintRuleTest { + @override + String get lintRule => 'use_is_even_rather_than_modulo'; + + test_isEven() async { + await assertNoDiagnostics(r''' +var a = 1.isEven; +'''); + } + + test_isOdd() async { + await assertNoDiagnostics(r''' +var a = 2.isOdd; +'''); + } + + test_moduloThree_intTypedExpression() async { + await assertNoDiagnostics(r''' +var a = 3; +var b = a % 3 == 1; +'''); + } + + test_moduloTwoEqualEqualOne_intTypedExpression() async { + await assertDiagnostics(r''' +var a = 3; +var b = a % 2 == 0; +''', [ + lint(19, 10), + ]); + } + + test_moduloTwoEqualEqualOne_literalInt() async { + await assertDiagnostics(r''' +var a = 13 % 2 == 1; +''', [ + lint(8, 11), + ]); + } + + test_moduloTwoEqualEqualThree() async { + await assertNoDiagnostics(r''' +var a = 1 % 2 == 3 - 3; +'''); + } + + test_moduloTwoEqualEqualZero_literalInt() async { + await assertDiagnostics(r''' +var a = 1 % 2 == 0; +''', [ + lint(8, 10), + ]); + } + + test_moduloTwoGreaterOrEqualZero_literalInt() async { + await assertNoDiagnostics(r''' + bool a = 1 % 2 >= 0; +'''); + } + + test_moduloTwoNotEqualZero_intTypedExpression() async { + await assertNoDiagnostics(r''' + int number = 3; + bool d = number % 2 != 0; +'''); + } + + test_plus_intTypedExpression() async { + await assertNoDiagnostics(r''' +var a = 3; +var b = a + 2 == 0; +'''); + } + + test_undefinedClass() async { + await assertDiagnostics(r''' +Class tmp; +bool a = tmp % 2 == 0; +''', [ + // No lint + error(CompileTimeErrorCode.UNDEFINED_CLASS, 0, 5), + ]); + } +} diff --git a/pkg/linter/test/rules/use_key_in_widget_constructors_test.dart b/pkg/linter/test/rules/use_key_in_widget_constructors_test.dart new file mode 100644 index 00000000000..21bcd006f26 --- /dev/null +++ b/pkg/linter/test/rules/use_key_in_widget_constructors_test.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseKeyInWidgetConstructorsTest); + }); +} + +@reflectiveTest +class UseKeyInWidgetConstructorsTest extends LintRuleTest { + @override + bool get addFlutterPackageDep => true; + + @override + String get lintRule => 'use_key_in_widget_constructors'; + + test_constNamedConstructor_missingKey() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + const MyWidget.named(); +} +''', [ + lint(107, 5), + ]); + } + + test_constUnnamedConstructor_missingKey() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + const MyWidget(); +} +''', [ + lint(98, 8), + ]); + } + + test_factoryConstructor() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class MyWidget extends StatelessWidget { + MyWidget({super.key}); + factory MyWidget.fact() => MyWidget(); + @override + Widget build(BuildContext context) => Container(); +} +'''); + } + + test_missingConstructor() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class NoConstructorWidget extends StatefulWidget {} +''', [ + lint(55, 19), + ]); + } + + test_missingKey() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget(); +} +''', [ + lint(92, 8), + ]); + } + + test_missingKey_keyPassedToSuper() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget.superCall() : super(key: Key('')); +} +'''); + } + + test_privateClass() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +// ignore: unused_element +abstract class _PrivateWidget extends StatefulWidget {} +'''); + } + + test_privateConstructor() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget._private(); +} +'''); + } + + test_redirectingConstructor_withKey() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget.redirect() : this.withKey(key: Key('')); + MyWidget.withKey({Key? key}) : super(key: key ?? Key('')); +} +'''); + } + + test_simpleFormalParameter_notPassedToSuper() async { + await assertDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget.withKey({Key? key}); +} +''', [ + lint(101, 7), + ]); + } + + test_simpleFormalParameter_passedToSuper() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +abstract class MyWidget extends StatelessWidget { + MyWidget.withKey({Key? key}) : super(key: key ?? Key('')); +} +'''); + } + + test_superParameter() async { + await assertNoDiagnostics(r''' +import 'package:flutter/widgets.dart'; + +class OtherWidget extends StatelessWidget { + const OtherWidget({required super.key}); + + @override + Widget build(BuildContext context) => Container(); +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_late_for_private_fields_and_variables_test.dart b/pkg/linter/test/rules/use_late_for_private_fields_and_variables_test.dart new file mode 100644 index 00000000000..f6eef708226 --- /dev/null +++ b/pkg/linter/test/rules/use_late_for_private_fields_and_variables_test.dart @@ -0,0 +1,280 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseLateForPrivateFieldsAndVariablesTest); + }); +} + +@reflectiveTest +class UseLateForPrivateFieldsAndVariablesTest extends LintRuleTest { + @override + List get experiments => ['inline-class']; + + @override + String get lintRule => 'use_late_for_private_fields_and_variables'; + + test_extensionType_instanceField() async { + await assertDiagnostics(''' +extension type E(int i) { + int? _i; +} +''', [ + // No lint. + error(CompileTimeErrorCode.EXTENSION_TYPE_DECLARES_INSTANCE_FIELD, 33, 2), + error(WarningCode.UNUSED_FIELD, 33, 2), + ]); + } + + test_extensionType_staticField() async { + await assertDiagnostics(''' +extension type E(int i) { + static int? _i; +} +''', [ + error(WarningCode.UNUSED_FIELD, 40, 2), + lint(40, 2), + ]); + } + + test_instanceField_private() async { + await assertDiagnostics(''' +class C { + int? _i; // ignore: unused_field +} +''', [lint(17, 2)]); + } + + /// https://github.com/dart-lang/linter/issues/3823 + test_instanceField_private_enum() async { + await assertDiagnostics(''' +enum E { + a('a'), + b('b', 'c'); + + const E(this._v, [this._v2]); + + final String _v; + final String? _v2; +} +''', [ + // No lint. + error(WarningCode.UNUSED_FIELD, 83, 2), + error(WarningCode.UNUSED_FIELD, 103, 3), + ]); + } + + test_instanceField_private_inClassWithConstConstructor() async { + await assertNoDiagnostics(''' +class C { + const C([this._i]); + final int? _i; // ignore: unused_field +} +'''); + } + + test_instanceField_private_inEnum() async { + await assertNoDiagnostics(''' +enum E { + a(''); + const E([this._f]); + final String? _f; // ignore: unused_field +} +'''); + } + + @FailingTest(issue: 'https://github.com/dart-lang/linter/issues/2921') + test_instanceField_private_partOf() async { + newFile('$testPackageLibPath/part.dart', r''' +part 'test.dart'; + +class C { + final String? _s; + C(this._s); +} +'''); + await assertNoDiagnostics(''' +part of 'part.dart'; +'''); + } + + test_instanceField_public() async { + await assertNoDiagnostics(''' +class C { + int? i; +} +'''); + } + + /// https://github.com/dart-lang/linter/issues/4180 + test_patternAssignment_field() async { + await assertDiagnostics(''' +class C { + int? _i; + void m() { + _i?.abs(); + (_i, ) = (null, ); + } +} +''', [ + // No lint. + error(CompileTimeErrorCode.PATTERN_ASSIGNMENT_NOT_LOCAL_VARIABLE, 54, 2), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4180 + test_patternAssignment_topLevel() async { + await assertDiagnostics(''' +int? _i; +m() { + _i?.abs(); + (_i, ) = (null, ); +} +''', [ + // No lint. + error(CompileTimeErrorCode.PATTERN_ASSIGNMENT_NOT_LOCAL_VARIABLE, 31, 2), + ]); + } + + test_staticField_private_onExtension() async { + await assertDiagnostics(''' +extension E on int { + static int? _i; // ignore: unused_field +} +''', [lint(35, 2)]); + } + + // TODO(srawlins): Add test_staticField_private_onClass. + + test_staticField_public_onPrivateExtension() async { + await assertDiagnostics(''' +extension _E on int { + static int? i; // ignore: unused_field +} +''', [lint(36, 1)]); + } + + test_staticField_public_onPublicExtension() async { + await assertNoDiagnostics(''' +extension E on int { + static int? i; +} +'''); + } + + test_staticField_public_onUnnamedExtension() async { + await assertDiagnostics(''' +extension on int { + static int? i; // ignore: unused_field +} +''', [lint(33, 1)]); + } + + test_topLevel_assigned() async { + await assertDiagnostics(''' +int? _i; // ignore: unused_element +void f() { + _i = 1; +} +''', [lint(5, 2)]); + } + + test_topLevel_neverUsed() async { + await assertDiagnostics(''' +int? _i; // ignore: unused_element +''', [lint(5, 2)]); + } + + test_topLevel_onlyAssignedNull() async { + await assertNoDiagnostics(''' +int? _i; // ignore: unused_element +void f() { + _i = null; +} +'''); + } + + test_topLevel_onlyEqualityCompared() async { + await assertNoDiagnostics(''' +int? _i; // ignore: unused_element +f() { + _i == 1; +} +'''); + } + + test_topLevel_onlyNullAwareAccess() async { + await assertNoDiagnostics(''' +int? _i; // ignore: unused_element +f() { + _i?.abs(); +} +'''); + } + + test_topLevel_onlyNullChecked() async { + await assertDiagnostics(''' +int? _i; // ignore: unused_element +f() { + _i!.abs(); +} +''', [lint(5, 2)]); + } + + test_topLevel_onlyNullChecked_beforePassedAsArgument() async { + await assertDiagnostics(''' +int? _i; // ignore: unused_element +f(int i) { + f(_i!); +} +''', [lint(5, 2)]); + } + + test_topLevel_onlyNullTest() async { + await assertNoDiagnostics(''' +int? _i; // ignore: unused_element +f() { + if (_i != null) _i.toString(); +} +'''); + } + + test_topLevel_passedAsArgument() async { + await assertNoDiagnostics(''' +int? _i; +f(int? i) { + f(_i); +} +'''); + } + + test_topLevel_public() async { + await assertNoDiagnostics(''' +int? i; +void f() { + i = 1; +} +'''); + } + + test_useInPart() async { + newFile2('$testPackageRootPath/lib/lib.dart', ''' +part '$testFileName'; + +int? _i; +'''); + await assertDiagnostics(''' +part of 'lib.dart'; + +m() { + _i = 1; +} +''', [lint(24, 2)]); + } +} diff --git a/pkg/linter/test/rules/use_named_constants_test.dart b/pkg/linter/test/rules/use_named_constants_test.dart new file mode 100644 index 00000000000..b11b5e9eea9 --- /dev/null +++ b/pkg/linter/test/rules/use_named_constants_test.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseNamedConstantsTest); + }); +} + +@reflectiveTest +class UseNamedConstantsTest extends LintRuleTest { + @override + String get lintRule => 'use_named_constants'; + + /// https://github.com/dart-lang/linter/issues/4201 + test_constantPattern_ifCase() async { + await assertDiagnostics(r''' +class A { + const A(this.value); + final int value; + + static const zero = A(0); +} + +void f(A a) { + if (a case const A(0)) {} +} +''', [ + lint(117, 4), + ]); + } + + /// https://github.com/dart-lang/linter/issues/4201 + test_constantPattern_switch() async { + await assertDiagnostics(r''' +class A { + const A(this.value); + final int value; + + static const zero = A(0); + static const one = A(1); +} + +void f(A a) { + switch (a) { + case const A(1): + } +} +''', [ + lint(155, 4), + ]); + } + + test_duplicate_inDefinition() async { + await assertNoDiagnostics(r''' +class A { + const A(int value); + static const zero = A(0); + static const zeroAgain = A(0); +} +'''); + } + + test_reconstructed_sameAsPrivateName() async { + await assertDiagnostics(r''' +void f() { + const A(1); +} +class A { + const A(int value); + // ignore: unused_field + static const _zero = A(0); +} +''', [ + lint(13, 10), + ]); + } + + test_reconstructed_sameAsPublicName_explicitConst() async { + await assertDiagnostics(r''' +void f() { + const A(0); +} +class A { + const A(int value); + static const zero = A(0); +} +''', [ + lint(13, 10), + ]); + } + + test_reconstructed_sameAsPublicName_implicitConst() async { + await assertDiagnostics(r''' +void f() { + const a = A(0); +} +class A { + const A(int value); + static const zero = A(0); +} +''', [ + lint(23, 4), + ]); + } + + test_usesNamed() async { + await assertNoDiagnostics(r''' +void f() { + A.zero; +} +class A { + const A(int value); + static const zero = A(0); +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_rethrow_when_possible_test.dart b/pkg/linter/test/rules/use_rethrow_when_possible_test.dart new file mode 100644 index 00000000000..533bad213a0 --- /dev/null +++ b/pkg/linter/test/rules/use_rethrow_when_possible_test.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseRethrowWhenPossibleTest); + }); +} + +@reflectiveTest +class UseRethrowWhenPossibleTest extends LintRuleTest { + @override + String get lintRule => 'use_rethrow_when_possible'; + + test_catchError_throwSameError() async { + await assertDiagnostics(r''' +void f() { + try {} catch (e) { + throw e; + } +} +''', [ + lint(36, 7), + ]); + } + + test_catchErrorAndStackTrace_throwSameError() async { + await assertDiagnostics(r''' +void f() { + try {} catch (e, stackTrace) { + print(stackTrace); + throw e; + } +} +''', [ + lint(71, 7), + ]); + } + + test_rethrow() async { + await assertNoDiagnostics(r''' +void f() { + try {} catch (e) { + rethrow; + } +} +'''); + } + + test_throwDifferentError() async { + await assertNoDiagnostics(r''' +void f() { + try {} catch (e1) { + try {} catch (e2) { + throw e1; + } + } +} +'''); + } + + test_throwNewError() async { + await assertNoDiagnostics(r''' +void f() { + try {} catch (e) { + throw Exception(); + } +} +'''); + } + + test_throwUsedAsAnExpression1() async { + await assertNoDiagnostics(r''' +void f() { + try {} catch (e) { + 1 == 2 ? e.toString() : throw e; + } +} +'''); + } + + test_throwUsedAsAnExpression2() async { + await assertNoDiagnostics(r''' +void f() { + try {} catch (e) { + print(throw e); + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_setters_to_change_properties_test.dart b/pkg/linter/test/rules/use_setters_to_change_properties_test.dart new file mode 100644 index 00000000000..e3d92d7013c --- /dev/null +++ b/pkg/linter/test/rules/use_setters_to_change_properties_test.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + // TODO(srawlins): Add test with setter-like with multiple statements; add + // test with non-trivial right side (`this.x = x + 1`). + defineReflectiveTests(UseSettersToChangePropertiesTest); + }); +} + +@reflectiveTest +class UseSettersToChangePropertiesTest extends LintRuleTest { + @override + String get lintRule => 'use_setters_to_change_properties'; + + test_setterLike_expressionBody() async { + await assertDiagnostics(r''' +abstract class A { + int x = 0; + void setX(int x) => this.x = x; +} +''', [ + lint(39, 4), + ]); + } + + test_setterLike_blockBody() async { + await assertDiagnostics(r''' +abstract class A { + int x = 0; + void setX(int x) { + this.x = x; + } +} +''', [ + lint(39, 4), + ]); + } + + test_combo() async { + await assertNoDiagnostics(r''' +abstract class A { + int x = 0; + void setX(int x) => this.x += x; +} +'''); + } + + test_inheritedFromSuperclass() async { + await assertNoDiagnostics(r''' +abstract class A { + void setX(int x); +} + +class B extends A { + int x = 0; + + void setX(int x) { + this.x = x; + } +} +'''); + } + + test_abstract() async { + await assertNoDiagnostics(r''' +abstract class A { + void setX(int x); +} +'''); + } + + test_inheritedFromSuperInterface() async { + await assertNoDiagnostics(r''' +abstract class A { + void setX(int x); +} + +class B implements A { + int x = 0; + + void setX(int x) { + this.x = x; + } +} +'''); + } + + test_extension() async { + await assertDiagnostics(r''' +class A { + int x = 0; +} + +extension E on A { + void setX(int x) { + this.x = x; + } +} +''', [ + lint(52, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/use_string_buffers_test.dart b/pkg/linter/test/rules/use_string_buffers_test.dart new file mode 100644 index 00000000000..8d5e1d65bf6 --- /dev/null +++ b/pkg/linter/test/rules/use_string_buffers_test.dart @@ -0,0 +1,221 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseStringBuffersTest); + }); +} + +@reflectiveTest +class UseStringBuffersTest extends LintRuleTest { + @override + String get lintRule => 'use_string_buffers'; + + test_field_nonString_plus() async { + await assertNoDiagnostics(r''' +class B { + operator +(B other) => this; + + void m() { + B b = B(); + for (var i = 0; i < 10; i++) { + b = b + this; + } + } +} +'''); + } + + test_field_nonString_plusEquals() async { + await assertNoDiagnostics(r''' +class B { + operator +(B other) => this; + + void m() { + B b = B(); + for (var i = 0; i < 10; i++) { + b += this; + } + } +} +'''); + } + + test_field_plus_bufferReference() async { + await assertNoDiagnostics(r''' +class A { + String buffer = ''; + + void foo() { + buffer = buffer + buffer; + } +} +'''); + } + + test_field_plusEquals_nonStringLiteral() async { + await assertDiagnostics(r''' +class A { + String buffer = ''; + + void foo(int n) { + int aux = n; + while (aux-- > 0) { + buffer += ''.toLowerCase(); + } + } +} +''', [ + lint(100, 26), + ]); + } + + test_field_plusEquals_stringLiteral() async { + await assertDiagnostics(r''' +class A { + String buffer = ''; + + void foo(int n) { + int aux = n; + while (aux-- > 0) { + buffer += 'a'; + } + } +} +''', [ + lint(100, 13), + ]); + } + + test_localVariable_assignment_interpolatedStringLiteralAsPrefix() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (int i = 0; i < 10; i++) { + buffer = '${buffer}a'; + } +} +''', [ + lint(69, 6), + ]); + } + + test_localVariable_assignment_interpolatedStringLiteralAsPrefixWithPlus() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (int i = 0; i < 10; i++) { + buffer = '${buffer + 'a'}a'; + } +} +''', [ + lint(69, 6), + ]); + } + + test_localVariable_assignment_interpolatedStringLiteralNotAsPrefix() async { + await assertNoDiagnostics(r''' +void foo() { + var buffer = ''; + for (int i = 0; i < 10; i++) { + buffer = 'a$buffer'; + } +} +'''); + } + + test_localVariable_doLoop_plusEquals_stringLiteral() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + do { + buffer += 'a'; + } while (buffer.length < 10); +} +''', [ + lint(43, 13), + ]); + } + + test_localVariable_plus_stringLiteral() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (int i = 0; i < 10; i++) { + buffer = buffer + 'a'; + } +} +''', [ + lint(69, 6), + ]); + } + + test_localVariable_plusEquals_nonStringLiteral() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (final s in ['a']) { + buffer += s; + } +} +''', [ + lint(63, 11), + ]); + } + + test_localVariable_plusEquals_nonStringLiteral_parenthesized() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (final s in ['a']) { + (buffer += s); + } +} +''', [ + lint(64, 11), + ]); + } + + test_localVariable_plusEquals_stringLiteral() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + for (int i = 0; i < 10; i++) { + buffer += 'a'; + } +} +''', [ + lint(69, 13), + ]); + } + + test_localVariable_whileLoop_plusEquals_stringLiteral() async { + await assertDiagnostics(r''' +void foo() { + var buffer = ''; + while (buffer.length < 10) { + buffer += 'a'; + } +} +''', [ + lint(67, 13), + ]); + } + + test_loopVariable_plusEquals_nonStringLiteral() async { + await assertNoDiagnostics(r''' +void foo() { + for (final s in [ 'a', 'b']) { + var buffer = ''; + buffer += s; + } +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_string_in_part_of_directives.dart b/pkg/linter/test/rules/use_string_in_part_of_directives.dart new file mode 100644 index 00000000000..3ddca8ab64b --- /dev/null +++ b/pkg/linter/test/rules/use_string_in_part_of_directives.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseStringInPartOfDirectivesTest); + }); +} + +@reflectiveTest +class UseStringInPartOfDirectivesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => 'use_string_in_part_of_directives'; + + test_part_of_with_string() async { + newFile2('$testPackageRootPath/lib/lib.dart', ''' +part '$testFileName'; +'''); + await assertNoDiagnostics(r''' +part of 'lib.dart'; +'''); + } + + test_part_of_with_library_name() async { + newFile2('$testPackageRootPath/lib/lib.dart', ''' +library lib; +part '$testFileName'; +'''); + await assertDiagnostics( + r''' +part of lib; +''', + [ + lint(0, 12), + ], + ); + } +} diff --git a/pkg/linter/test/rules/use_super_parameters_test.dart b/pkg/linter/test/rules/use_super_parameters_test.dart new file mode 100644 index 00000000000..f761025088b --- /dev/null +++ b/pkg/linter/test/rules/use_super_parameters_test.dart @@ -0,0 +1,436 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseSuperParametersTest); + }); +} + +@reflectiveTest +class UseSuperParametersTest extends LintRuleTest { + @override + String get lintRule => 'use_super_parameters'; + + test_functionTypedFormalParameter() async { + await assertDiagnostics(r''' +class A { + A(int f(int i)); +} +class B extends A { + B(int f(int i)) : super(f); +} +''', [ + lint(53, 1), + ]); + } + + test_named() async { + await assertDiagnostics(r''' +class A { + const A({int? x, int? y}); +} +class B extends A { + const B({int? x, int? y}) : super(x: x, y: y); +} +''', [ + lint(69, 1), + ]); + } + + Future test_named_oneWithNameChange() async { + await assertDiagnostics(''' +class A { + A({int? x, int? y}); +} +class B extends A { + B({int? x, int? z}) : super(x: x, y: z); +} +''', [ + lint(57, 1), + ]); + } + + test_named_someReferencedInBody() async { + await assertDiagnostics(r''' +class A { + A({int? x, int? y}); +} +class B extends A { + B({int? x, int? y}) : super(x: x, y: y) { + print(x); + } +} +''', [ + lint(57, 1, messageContains: "Convert 'y' to a super parameter"), + ]); + } + + test_named_thisParameter() async { + await assertDiagnostics(r''' +class A { + A({int? x, int? y}); +} +class B extends A { + int? x; + B({this.x, int? y}) : super(x:x, y:y); +} +''', [ + lint(67, 1, messageContains: "Convert 'y' to a super parameter."), + ]); + } + + test_no_lint_forwardedOutOfOrder() async { + await assertNoDiagnostics(r''' +class B { + final int x; + final int y; + B(this.x, this.y); +} +class C extends B { + C(int x, int y) : super(y, x); +} +'''); + } + + test_no_lint_invalid_fieldFormalParameter() async { + await assertNoDiagnostics(r''' +class A { + A(int x); +} +class B extends A { + B(int x) : super(x) { + print(x); + } +} +'''); + } + + Future test_no_lint_named_nameChange() async { + await assertNoDiagnostics(''' +class A { + A({int? x}); +} +class B extends A { + B({int? y}) : super(x: y); +} +'''); + } + + test_no_lint_named_noSuperInvocation() async { + await assertNoDiagnostics(r''' +class A { + A({int x = 0}); +} +class B extends A { + B({int x = 1}); +} +'''); + } + + test_no_lint_named_notGenerative() async { + await assertNoDiagnostics(r''' +class A { + A({required int x}); +} +class B extends A { + static List instances = []; + factory B({required int x}) => instances[x]; +} +'''); + } + + test_no_lint_named_notPassed_unreferenced() async { + await assertNoDiagnostics(r''' +class A { + A({int x = 0}); +} +class B extends A { + B({int x = 0}) : super(x: 0); +} +'''); + } + + test_no_lint_named_notPassed_usedInExpression() async { + await assertNoDiagnostics(r''' +class A { + A({String x = ''}); +} +class B extends A { + B({required Object x}) : super(x: x.toString()); +} +'''); + } + + test_no_lint_named_passedAsPositional() async { + await assertNoDiagnostics(r''' +class A { + A(String x); +} +class B extends A { + B({required String x}) : super(x); +} +'''); + } + + test_no_lint_nonSimpleIdentifierArg() async { + await assertNoDiagnostics(r''' +class A { + A(int x, int y, [int? z]); +} +class B extends A { + B(int a, int b) : super(a, 2, b); +} +'''); + } + + test_no_lint_notAllForwarded() async { + await assertNoDiagnostics(r''' +class B { + final int x; + final int y; + B(this.x, this.y); +} +class C extends B { + C(int x) : super(x, 0); +} +'''); + } + + test_no_lint_positionalReferencedInBody() async { + await assertNoDiagnostics(r''' +class A { + A.a(int x, int? y); +} +class B extends A { + B(int x, int? y) : super.a(x, y) { + print(x); + } +} +'''); + } + + test_no_lint_positionalThisParameter() async { + await assertNoDiagnostics(r''' +class A { + A(int x, int y); +} +class B extends A { + int x; + B(this.x, int y) : super(x, y); +} +'''); + } + + test_no_lint_referencedInBody() async { + await assertNoDiagnostics(r''' +class A { + A.a(int x, {int? y}); +} +class B extends A { + B(int x, {int? y}) : super.a(x, y: y) { + print(x); + print(y); + } +} +'''); + } + + test_no_lint_referencedInBody_named() async { + await assertNoDiagnostics(r''' +class A { + A({int? x}); +} +class B extends A { + B({int? x}) : super(x: x) { + print(x); + } +} +'''); + } + + test_no_lint_referencedInBody_positional() async { + await assertNoDiagnostics(r''' +class A { + int x; + A(this.x); +} +class B extends A { + int y; + B(this.y) : super(y); +} +'''); + } + + test_no_lint_requiredPositional_namedInSuper() async { + await assertNoDiagnostics(r''' +class A { + A({int? x}); +} +class B extends A { + B(int x) : super(x: x); +} +'''); + } + + test_no_lint_requiredPositional_noSuperInvocation() async { + await assertNoDiagnostics(r''' +class A { + A(); +} +class B extends A { + B(int x); +} +'''); + } + + test_no_lint_requiredPositional_notGenerative() async { + await assertNoDiagnostics(r''' +class A { + A(int x); +} +class B extends A { + static List instances = []; + factory B(int x) => instances[x]; +} +'''); + } + + test_no_lint_requiredPositional_notPassed_unreferenced() async { + await assertNoDiagnostics(r''' +class A { + A(int x); +} +class B extends A { + B(int x) : super(0); +} +'''); + } + + test_no_lint_requiredPositional_notPassed_usedInExpression() async { + await assertNoDiagnostics(r''' +class A { + A(String x); +} +class B extends A { + B(Object x) : super(x.toString()); +} +'''); + } + + test_nonForwardingNamed() async { + await assertDiagnostics(r''' +class A { + A(int x, {int? foo}); +} +class B extends A { + B(int x, {int? foo}) : super(x, foo: 0); +} +''', [ + lint(59, 1), + ]); + } + + test_optionalPositional_inSuper() async { + await assertDiagnostics(r''' +class A { + A(int x, [int? y]); +} +class B extends A { + B(int x) : super(x); +} +''', [ + lint(56, 1), + ]); + } + + test_optionalPositional_singleSuperParameter() async { + await assertDiagnostics(r''' +class A { + A(int x); +} +class B extends A { + B([int x = 0]) : super(x); +} +''', [ + lint(46, 1), + ]); + } + + /// https://github.com/dart-lang/linter/issues/3569 + test_repeatedParam() async { + await assertNoDiagnostics(r''' +class Rect { + Rect(this.width, this.height); + + final double width; + final double height; +} + +class Square extends Rect { + Square(double dimension) : super(dimension, dimension); +} +'''); + } + + test_requiredPositional_allConvertible() async { + await assertDiagnostics(r''' +class B { + final int foo; + final int bar; + B(this.foo, this.bar); +} +class C extends B { + C(int foo, int bar) : super(foo, bar); +} +''', [ + lint(93, 1), + ]); + } + + test_requiredPositional_mixedSuperParameters() async { + await assertDiagnostics(r''' +class A { + A(int x, {int? y}); +} +class B extends A { + B(int x, int y) : super(x, y: y); +} +''', [ + lint(56, 1), + ]); + } + + test_requiredPositional_someConvertible() async { + await assertDiagnostics(r''' +class B { + final int foo; + final int bar; + B(this.foo, this.bar); +} +class C extends B { + C(int baz, int foo, int bar) : super(foo, bar); +} +''', [ + lint(93, 1), + ]); + } + + test_requiredPositional_withNamed() async { + await assertDiagnostics(r''' +class A { + A(int x, {int? y}); +} +class B extends A { + B(int x, {int? y}) : super(x, y: y); +} +''', [ + lint(56, 1), + ]); + } +} diff --git a/pkg/linter/test/rules/use_test_throws_matchers_test.dart b/pkg/linter/test/rules/use_test_throws_matchers_test.dart new file mode 100644 index 00000000000..7728a37f3e8 --- /dev/null +++ b/pkg/linter/test/rules/use_test_throws_matchers_test.dart @@ -0,0 +1,98 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseTestThrowsMatchersTest); + }); +} + +@reflectiveTest +class UseTestThrowsMatchersTest extends LintRuleTest { + @override + String get lintRule => 'use_test_throws_matchers'; + + @override + void setUp() { + super.setUp(); + var testApiPath = '$workspaceRootPath/test_api'; + var packageConfigBuilder = PackageConfigFileBuilder(); + packageConfigBuilder.add( + name: 'test_api', + rootPath: testApiPath, + ); + writeTestPackageConfig(packageConfigBuilder); + newFile('$testApiPath/lib/src/frontend/expect.dart', r''' +void expect(dynamic actual, dynamic matcher) {} + +Never fail(String message) {} +'''); + newFile('$testApiPath/lib/test_api.dart', r''' +export 'src/frontend/expect.dart'; +'''); + } + + test_failInTry() async { + await assertDiagnostics(r''' +import 'package:test_api/test_api.dart'; + +void f() { + try { + f(); + fail('fail'); + } catch (e) {} +} +''', [ + lint(74, 13), + ]); + } + + test_failWithExpectInCatch() async { + await assertNoDiagnostics(r''' +import 'package:test_api/test_api.dart'; + +void f() { + try { + f(); + fail('fail'); + } catch (e) { + expect(e, null); + } finally { + print('hello'); + } +} +'''); + } + + test_failWithExpectInOnCatch() async { + await assertNoDiagnostics(r''' +import 'package:test_api/test_api.dart'; + +void f() { + try { + f(); + fail('fail'); + } on Exception catch (e) { + expect(e, null); + } catch (e) { + expect(e, null); + } +} +'''); + } + + test_noFail() async { + await assertNoDiagnostics(r''' +void f() { + try { + f(); + } catch (e) {} +} +'''); + } +} diff --git a/pkg/linter/test/rules/use_to_and_as_if_applicable_test.dart b/pkg/linter/test/rules/use_to_and_as_if_applicable_test.dart new file mode 100644 index 00000000000..603472a6049 --- /dev/null +++ b/pkg/linter/test/rules/use_to_and_as_if_applicable_test.dart @@ -0,0 +1,267 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseToAndAsIfApplicableTest); + }); +} + +@reflectiveTest +class UseToAndAsIfApplicableTest extends LintRuleTest { + @override + String get lintRule => 'use_to_and_as_if_applicable'; + + test_asOther_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + A asOther() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_asX_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + A asA() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_asx_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + A asa() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(14, 3), + ]); + } + + test_asX_private_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + // ignore: unused_element + A _asA() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_asx_private_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + // ignore: unused_element + A _asa() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(42, 4), + ]); + } + + test_getX_argumentIsOther_extends() async { + await assertNoDiagnostics(r''' +abstract class C { + A getA(); +} +class D extends C { + A getA() => A.from(B()); +} + +class A { + A.from(B _); +} +class B {} +'''); + } + + test_getX_argumentIsOther_implements() async { + await assertNoDiagnostics(r''' +abstract class C { + A getA(); +} +class D implements C { + A getA() => A.from(B()); +} + +class A { + A.from(B _); +} +class B {} +'''); + } + + test_getX_argumentIsOther_superConstraint() async { + await assertNoDiagnostics(r''' +abstract class C { + A getA(); +} +mixin D on C { + A getA() => A.from(B()); +} + +class A { + A.from(B _); +} +class B {} +'''); + } + + test_namedOtherwise_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + A foo() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(14, 3), + ]); + } + + test_namedOtherwise_private_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + // ignore: unused_element + A _foo() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(42, 4), + ]); + } + + test_namedOtherwise_private_hasParameters_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + // ignore: unused_element + A _foo(int a) { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_toOther_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + A toList() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_toX_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + A toA() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_tox_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + A toa() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(14, 3), + ]); + } + + test_toX_private_argumentIsThis() async { + await assertNoDiagnostics(r''' +class B { + // ignore: unused_element + A _toA() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +'''); + } + + test_tox_private_argumentIsThis() async { + await assertDiagnostics(r''' +class B { + // ignore: unused_element + A _toa() { + return A.from(this); + } +} + +class A { + A.from(B _); +} +''', [ + lint(42, 4), + ]); + } +} diff --git a/pkg/linter/test/rules/valid_regexps_test.dart b/pkg/linter/test/rules/valid_regexps_test.dart new file mode 100644 index 00000000000..823bc4a0a57 --- /dev/null +++ b/pkg/linter/test/rules/valid_regexps_test.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ValidRegexpsTest); + }); +} + +@reflectiveTest +class ValidRegexpsTest extends LintRuleTest { + @override + String get lintRule => 'valid_regexps'; + + test_emojis() async { + // https://stackoverflow.com/questions/61151471/regexp-for-unicode-13-emojis + await assertNoDiagnostics(r''' +var e = RegExp( + r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])', + unicode: true); +'''); + } + + test_interpolation() async { + await assertNoDiagnostics(r''' +var r = ''; +var s = RegExp('( $r'); +'''); + } + + test_invalid() async { + await assertDiagnostics(r''' +var s = RegExp('('); +''', [ + lint(15, 3), + ]); + } + + test_valid() async { + await assertNoDiagnostics(r''' +var s = RegExp('[(]'); +'''); + } +} diff --git a/pkg/linter/test/rules/void_checks_test.dart b/pkg/linter/test/rules/void_checks_test.dart new file mode 100644 index 00000000000..208eff40f65 --- /dev/null +++ b/pkg/linter/test/rules/void_checks_test.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(VoidChecksTest); + }); +} + +@reflectiveTest +class VoidChecksTest extends LintRuleTest { + @override + String get lintRule => 'void_checks'; + + test_constructorArgument_genericParameter() async { + await assertDiagnostics(r''' +void f(dynamic p) { + A.c(p); +} +class A { + T value; + A.c(this.value); +} +''', [ + lint(32, 1), + ]); + } + + test_extraPositionalArgument() async { + await assertDiagnostics(r''' +missing_parameter_for_argument() { + void foo() {} + foo(0); +} +''', [ + // No lint + error(CompileTimeErrorCode.EXTRA_POSITIONAL_ARGUMENTS, 57, 1), + ]); + } + + test_functionArgument_FutureOrVoidParameter_dynamicArgument() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f(dynamic p) { + m(p); +} +void m(FutureOr arg) {} +'''); + } + + test_functionArgument_FutureOrVoidParameter_FutureOrVoidArgument() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f(FutureOr p) { + m(p); +} +void m(FutureOr arg) {} +'''); + } + + test_functionArgument_FutureOrVoidParameter_FutureVoidArgument() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f(Future p) { + m(p); +} +void m(FutureOr arg) {} +'''); + } + + test_functionArgument_FutureOrVoidParameter_nullArgument() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f() { + m(null); +} +void m(FutureOr arg) {} +'''); + } + + test_functionArgument_voidParameter_dynamicArgument() async { + await assertDiagnostics(r''' +void f(dynamic p) { + m(p); +} +void m(void arg) {} +''', [ + lint(24, 1), + ]); + } + + test_functionArgument_voidParameter_named() async { + await assertDiagnostics(r''' +void f(dynamic p) { + m(p: p); +} +void m({required void p}) {} +''', [ + lint(27, 1), + ]); + } + + test_functionArgument_voidParameter_optional() async { + await assertDiagnostics(r''' +void f(dynamic p) { + m(p); +} +void m([void v]) {} +''', [ + lint(24, 1), + ]); + } + + // https://github.com/dart-lang/linter/issues/2685 + test_functionType_FutureOrVoidReturnType_Never() async { + await assertNoDiagnostics(r''' +import 'dart:async'; +void f() { + foo(() { + fail(); // OK + }); +} + +void foo(FutureOr Function() p) {} +Never fail() { throw ''; } +'''); + } + + /// https://github.com/dart-lang/linter/issues/4019 + test_future_dynamic() async { + await assertNoDiagnostics(r''' +import 'dart:async'; + +void f(FutureOr? arg) { + Future? future; + f(future); +} +'''); + } + + test_listPattern_local() async { + await assertDiagnostics(r''' +void f() { + void p; + [p] = [7]; + return p; +} +''', [ + lint(24, 1), + ]); + } + + test_listPattern_param() async { + await assertDiagnostics(r''' +void f(void p) { + [p] = [7]; +} +''', [ + lint(20, 1), + ]); + } + + test_recordPattern() async { + await assertDiagnostics(r''' +void f(void p) { + (p, ) = (7, ); +} +''', [ + lint(20, 1), + ]); + } + + test_returnOfInvalidType() async { + await assertDiagnostics(r''' +void bug2813() { + return 1; +} +''', [ + // No lint + error(CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION, 26, 1), + ]); + } + + // https://github.com/dart-lang/linter/issues/2685 + test_returnTypeVoid_Never() async { + await assertNoDiagnostics(r''' +void f(Future p) { + p.then((_) { + fail(); + }); +} + +Never fail() { throw ''; } +'''); + } + + test_returnTypeVoid_throw() async { + await assertNoDiagnostics(r''' +void f(Future p) { + p.then((_) { + throw ''; + }); +} +'''); + } + + test_setterArgument_genericParameter() async { + await assertDiagnostics(r''' +void f(A a, dynamic p) { + a.f = p; +} +class A { + set f(T value) {} +} +''', [ + lint(33, 7), + ]); + } +} diff --git a/pkg/linter/test/test_constants.dart b/pkg/linter/test/test_constants.dart new file mode 100644 index 00000000000..3d27fc358f2 --- /dev/null +++ b/pkg/linter/test/test_constants.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as path; + +final String integrationTestDir = path.join('test_data', 'integration'); +final String ruleTestDataDir = path.join('test_data', 'rules'); +final String ruleTestDir = path.join('test', 'rules'); +final String testConfigDir = path.join('test', 'configs'); diff --git a/pkg/linter/test/unmocked_sdk_rule_test.dart b/pkg/linter/test/unmocked_sdk_rule_test.dart new file mode 100644 index 00000000000..a97e94d322a --- /dev/null +++ b/pkg/linter/test/unmocked_sdk_rule_test.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'rule_test.dart'; +import 'test_constants.dart'; + +void main() { + group('un-mocked', () { + // Validate that rule tests produce the expected results when run against + // an un-mocked SDK. + for (var entry + in Directory(p.join(ruleTestDataDir, 'unmocked')).listSync()) { + if (entry is! File) continue; + + var ruleName = p.basenameWithoutExtension(entry.path); + testRule(ruleName, entry, useMockSdk: false); + } + }); +} diff --git a/pkg/linter/test/util/annotation.dart b/pkg/linter/test/util/annotation.dart new file mode 100644 index 00000000000..f2920188e56 --- /dev/null +++ b/pkg/linter/test/util/annotation.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Deprecated( + 'Prefer importing `package:linter/src/test_utilities/annotation.dart` directly') +export 'package:linter/src/test_utilities/annotation.dart'; diff --git a/pkg/linter/test/util/annotation_matcher.dart b/pkg/linter/test/util/annotation_matcher.dart new file mode 100644 index 00000000000..53e2a0130e6 --- /dev/null +++ b/pkg/linter/test/util/annotation_matcher.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/error/error.dart'; +import 'package:linter/src/test_utilities/annotation.dart'; +import 'package:test/test.dart'; + +AnnotationMatcher matchesAnnotation( + String? message, ErrorType type, int lineNumber) => + AnnotationMatcher(Annotation(message, type, lineNumber)); + +class AnnotationMatcher extends Matcher { + final Annotation _expected; + + AnnotationMatcher(this._expected); + + @override + Description describe(Description description) => + description.addDescriptionOf(_expected); + + @override + bool matches(item, Map matchState) => item is Annotation && _matches(item); + + bool _matches(Annotation other) { + // Only test messages if they're specified in the expectation + if (_expected.message != null) { + if (_expected.message != other.message) { + return false; + } + } + // Similarly for highlighting + if (_expected.column != null) { + if (_expected.column != other.column || + _expected.length != other.length) { + return false; + } + } + return _expected.type == other.type && + _expected.lineNumber == other.lineNumber; + } +} diff --git a/pkg/linter/test/util/rule_debug.dart b/pkg/linter/test/util/rule_debug.dart new file mode 100644 index 00000000000..29ee59a7996 --- /dev/null +++ b/pkg/linter/test/util/rule_debug.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../rule_test.dart'; +import '../test_constants.dart'; + +/// Solo rule test runner. Handy for debugging until `dart test` supports +/// VM debugging (https://github.com/dart-lang/test/issues/50). +/// +/// Run, for example, like so: +/// dart test/util/rule_debug.dart valid_regexps +/// +/// To simply *run* a solo test, consider using `dart test -N`: +/// dart test -N valid_regexps +/// +void main(List args) { + var ruleName = args.first; + var dir = Directory(ruleTestDataDir).absolute; + testRule(ruleName, File(p.join(dir.path, '$ruleName.dart'))); +} diff --git a/pkg/linter/test/util/test_resource_provider.dart b/pkg/linter/test/util/test_resource_provider.dart new file mode 100644 index 00000000000..7c3fb4f509a --- /dev/null +++ b/pkg/linter/test/util/test_resource_provider.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Deprecated( + 'Prefer importing `package:linter/src/test_utilities/test_resource_provider.dart` directly') +export 'package:linter/src/test_utilities/test_resource_provider.dart'; diff --git a/pkg/linter/test/util/test_utils.dart b/pkg/linter/test/util/test_utils.dart new file mode 100644 index 00000000000..a3f86cbbca6 --- /dev/null +++ b/pkg/linter/test/util/test_utils.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:analyzer/src/lint/io.dart'; +import 'package:linter/src/rules.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void setUpSharedTestEnvironment() { + // Redirect output. + outSink = MockIOSink(); + registerLintRules(); +} + +void testEach(Iterable values, bool Function(T s) f, Matcher m) { + for (var s in values) { + test('"$s"', () => expect(f(s), m)); + } +} diff --git a/pkg/linter/test/utils_test.dart b/pkg/linter/test/utils_test.dart new file mode 100644 index 00000000000..a93aa33a68e --- /dev/null +++ b/pkg/linter/test/utils_test.dart @@ -0,0 +1,210 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/utils.dart'; +import 'package:test/test.dart'; + +import 'util/test_utils.dart'; + +void main() { + group('isDartFileName', () { + testEach(['foo.dart'], isDartFileName, isTrue); + testEach([ + 'foo.d', 'foo', + // Analysis server cares about case. + 'baz.DART', + ], isDartFileName, isFalse); + }); + + group('isValidPackageName', () { + testEach([ + 'foo', + '_foo', + '__foo', + 'async', + ], isValidPackageName, isTrue); + testEach([ + 'break', // reserved word + 'fOO', + 'foo_', + 'foo_Bar', + 'fooBar', + 'Foo', + '_', + '__', + '1', + ], isValidPackageName, isFalse); + }); + + group('pubspec', () { + testEach(['pubspec.yaml', '_pubspec.yaml'], isPubspecFileName, isTrue); + testEach(['__pubspec.yaml', 'foo.yaml'], isPubspecFileName, isFalse); + }); + + group('camel case', () { + group('upper', () { + var good = [ + '_FooBar', + 'FooBar', + '_Foo', + 'Foo', + 'F', + 'FB', + 'F1', + 'FooBar1', + '\$Foo', + 'Bar\$', + 'Foo\$Generated', + 'Foo\$Generated\$Bar' + ]; + testEach(good, isCamelCase, isTrue); + var bad = ['fooBar', 'foo', 'f', '_f', 'F_B']; + testEach(bad, isCamelCase, isFalse); + }); + + group('CamelCaseString', () { + test('invalid creation', () { + expect(() => CamelCaseString('invalid'), + throwsA(TypeMatcher())); + }); + test('toString', () { + expect(CamelCaseString('CamelCase').toString(), 'CamelCase'); + }); + test('humanize', () { + expect(CamelCaseString('CamelCase').humanized, 'Camel Case'); + }); + }); + }); + + group('library prefixes', () { + const good = [ + 'foo_bar', + 'foo', + 'foo_bar_baz', + 'p', + 'p1', + 'p21', + 'p1ll0', + '_foo', + // Ensure multiple underscores don't cause issues (#940). + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa__aa', + 'abc_def_ghi_jkl_mnomnomno_abcabcabc__defdefdefdef__ghighighighi', + 'abc_____def____ghi_jkl__mnomnomno_abcabcabc__defdefdefdef___ghighighighi', + r'$foo', + '__foo', + r'$_foo', + r'$__foo', + ]; + testEach(good, isValidLibraryPrefix, isTrue); + + const bad = [ + 'JSON', + 'JS', + 'Math', + 'jsUtils', + 'foo_Bar', + 'F_B', + '1', + '1b', + r'foo$', + r'f$oo', + r'$$foo', + r'$_$foo', + r'_$foo', + '__', + '_', + '_1', + ]; + testEach(bad, isValidLibraryPrefix, isFalse); + }); + + group('lower_case_underscores', () { + var good = ['foo_bar', 'foo', 'foo_bar_baz', 'p', 'p1', 'p21', 'p1ll0']; + testEach(good, isLowerCaseUnderScore, isTrue); + + var bad = [ + 'Foo', + 'fooBar', + 'foo_Bar', + 'foo_', + '_f', + 'F_B', + 'JS', + 'JSON', + '1', + '1b', + ]; + testEach(bad, isLowerCaseUnderScore, isFalse); + }); + + group('isLowerCaseUnderScoreWithDots', () { + var good = [ + 'bwu_server.shared.datastore.some_file', + 'foo_bar.baz', + 'foo_bar', + 'foo.bar', + 'foo_bar_baz', + 'foo', + 'foo_', + 'foo.bar_baz.bang', + //See: https://github.com/flutter/flutter/pull/1996 + 'pointycastle.impl.ec_domain_parameters.gostr3410_2001_cryptopro_a', + 'a.b', + 'a.b.c', + 'p2.src.acme', + //https://github.com/dart-lang/linter/issues/1803 + '_', + '_f', + '__f', + '___f', + '_file.dart', + ]; + testEach(good, isLowerCaseUnderScoreWithDots, isTrue); + + var bad = [ + 'Foo', + 'fooBar.', + '.foo_Bar', + '_.', + '.', + 'F_B', + 'JS', + 'JSON', + ]; + + testEach(bad, isLowerCaseUnderScoreWithDots, isFalse); + }); + + group('lowerCamelCase', () { + var good = [ + 'fooBar', + 'foo', + 'f', + 'f1', + '_f', + '_foo', + '_', + 'F', + '__x', + '___x', + '\$foo', + 'bar\$', + 'foo\$Generated', + 'foo\$Generated\$Bar', + 'foo_', + ]; + testEach(good, isLowerCamelCase, isTrue); + + var bad = ['Foo', 'foo_bar', '_X']; + testEach(bad, isLowerCamelCase, isFalse); + }); + + group('isUpperCase', () { + var caps = List.generate(26, (i) => 'A'.codeUnitAt(0) + i); + testEach(caps, isUpperCase, isTrue); + + var bad = ['a', '1', 'z'].map((c) => c.codeUnitAt(0)); + testEach(bad, isUpperCase, isFalse); + }); +} diff --git a/pkg/linter/test/validate_format_test.dart b/pkg/linter/test/validate_format_test.dart new file mode 100644 index 00000000000..f2af0c64f5b --- /dev/null +++ b/pkg/linter/test/validate_format_test.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + test('validate source formatting', () async { + var result = await Process.run( + 'dart', ['format', '-o', 'none', '--set-exit-if-changed', '.']); + var violations = result.stdout.toString().split('\n') + ..removeWhere(lineIgnored); + expect(violations, isEmpty, reason: '''Some files need formatting. + +Run `dart format` and (re)commit.'''); + }); +} + +bool lineIgnored(String line) => + line.isEmpty || + line.startsWith('Changed test_data${Platform.pathSeparator}') || + line.startsWith('Formatted '); diff --git a/pkg/linter/test/validate_headers_test.dart b/pkg/linter/test/validate_headers_test.dart new file mode 100644 index 00000000000..ee49f03662e --- /dev/null +++ b/pkg/linter/test/validate_headers_test.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + group('check for copyright headers', () { + test('... in bin', () async { + await validate('bin'); + }); + test('... in lib', () async { + await validate('lib'); + }); + test('... in test', () async { + await validate('test'); + }); + test('... in tool', () async { + await validate('tool'); + }); + }); +} + +Future validate(String dir) async { + var violations = []; + await for (FileSystemEntity entity + in Directory(dir).list(recursive: true, followLinks: false)) { + if (entity is File && entity.path.endsWith('.dart')) { + var file = await entity.open(); + List bytes = await file.read(40); + var header = String.fromCharCodes(bytes); + if (!header.startsWith( + RegExp('// Copyright \\(c\\) 20[0-9][0-9], the Dart project'))) { + violations.add(entity.path); + } + } + } + expect(violations, isEmpty, reason: '''Files missing copyright headers. + +See CONTRIBUTING.md for format details.'''); +} diff --git a/pkg/linter/test/validate_incompatible_rules.dart b/pkg/linter/test/validate_incompatible_rules.dart new file mode 100644 index 00000000000..30634f34899 --- /dev/null +++ b/pkg/linter/test/validate_incompatible_rules.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('check for incompatible rules:', () { + registerLintRules(); + for (var rule in Analyzer.facade.registeredRules) { + for (var incompatibleRule in rule.incompatibleRules) { + test(rule.name, () async { + var referencedRule = Analyzer.facade.registeredRules + .firstWhere((r) => r.name == incompatibleRule); + expect(referencedRule, isNotNull, + reason: + 'No rule found for id: $incompatibleRule (check for typo?)'); + expect(referencedRule.incompatibleRules, contains(rule.name), + reason: + '$referencedRule should define ${rule.name} in `incompatibleRules` but does not.'); + }); + } + } + }); +} diff --git a/pkg/linter/test/validate_no_rule_description_references.dart b/pkg/linter/test/validate_no_rule_description_references.dart new file mode 100644 index 00000000000..063d3651941 --- /dev/null +++ b/pkg/linter/test/validate_no_rule_description_references.dart @@ -0,0 +1,23 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('check for rule message descriptions in tests:', () { + registerLintRules(); + for (var rule in Analyzer.facade.registeredRules) { + test(rule.name, () async { + var result = Process.runSync('grep', ['-R', rule.description, 'test']); + expect(result.stdout, isEmpty, + reason: + 'Tests that hardcode descriptions make lint messages hard to evolve.'); + }); + } + }); +} diff --git a/pkg/linter/test/validate_rule_description_format_test.dart b/pkg/linter/test/validate_rule_description_format_test.dart new file mode 100644 index 00000000000..2d24d48d718 --- /dev/null +++ b/pkg/linter/test/validate_rule_description_format_test.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:test/test.dart'; + +import 'util/test_utils.dart'; + +void main() { + const keywords = [ + 'GOOD', + 'BAD', + 'NOTE', + 'DEPRECATED', + 'EXCEPTION', + 'EXCEPTIONS', + ]; + + group('rule doc format', () { + setUp(setUpSharedTestEnvironment); + + var rules = Registry.ruleRegistry.rules; + test('(setup)', () { + expect(rules, isNotEmpty, + reason: + 'Ensure `registerLintRules()` is called before running this suite.'); + }); + + group('description - trailing periods', () { + for (var rule in rules) { + test('`${rule.name}` description', () { + expect(rule.description.endsWith('.'), isTrue, + reason: + "Rule description for ${rule.name} should end with a '.'"); + }); + } + }); + + group('details - no leading whitespace', () { + for (var rule in rules) { + test('`${rule.name}` details', () { + expect(rule.details.startsWith(RegExp(r'\s+')), isFalse, + reason: + 'Rule details for ${rule.name} should not have leading whitespace.'); + }); + } + }); + + group('details - bad first', () { + for (var rule in rules) { + test('`${rule.name}` bad example first', () { + var details = rule.details; + var lines = details.split('\n'); + var hasGood = false; + for (var line in lines) { + if (line.startsWith('**BAD:**')) { + if (hasGood) { + fail( + 'Rule details for ${rule.name} should have the BAD example before the GOOD one.'); + } + break; + } else if (line.startsWith('**GOOD:**')) { + hasGood = true; + } + } + }); + } + }); + + group('details - colon inside stars', () { + for (var rule in rules) { + test('`${rule.name}` colon inside stars', () { + var details = rule.details; + var lines = details.split('\n'); + + for (var line in lines) { + for (var keyword in keywords) { + var withStars = '**$keyword**'; + if (line.contains(withStars)) { + fail( + 'Rule details for ${rule.name} should have **$keyword:**, put the colon inside the stars.'); + } + } + } + }); + } + }); + + group('details - upper case keywords', () { + for (var rule in rules) { + test('`${rule.name}` upper case keywords', () { + var details = rule.details; + var lines = details.split('\n'); + + for (var line in lines) { + for (var keyword in keywords) { + var withStars = '**$keyword:**'; + if (line.toLowerCase().contains(withStars.toLowerCase()) && + !line.contains(withStars)) { + fail( + 'Rule details for ${rule.name} should have $withStars in upper case.'); + } + } + } + }); + } + }); + }); +} diff --git a/pkg/linter/test/validate_sdk_version_map.dart b/pkg/linter/test/validate_sdk_version_map.dart new file mode 100644 index 00000000000..3af37500641 --- /dev/null +++ b/pkg/linter/test/validate_sdk_version_map.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/rules.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + group('check to ensure all lints have versions in `sdk.yaml`', () { + var sinceFile = File('tool/since/sdk.yaml').readAsStringSync(); + var versionMap = loadYamlNode(sinceFile) as YamlMap; + registerLintRules(); + for (var rule in Analyzer.facade.registeredRules.map((r) => r.name)) { + test(rule, () async { + expect(versionMap.keys, contains(rule), + reason: "'$rule' should have and entry in `tool/since/sdk.yaml`."); + }); + } + }); +} diff --git a/pkg/linter/test/verify_checks_test.dart b/pkg/linter/test/verify_checks_test.dart new file mode 100644 index 00000000000..25ce36d461a --- /dev/null +++ b/pkg/linter/test/verify_checks_test.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +import '../tool/checks/driver.dart'; + +main() { + group('custom checks', () { + var checkNames = customChecks.map((c) => c.name).join(', '); + test(checkNames, () async { + var failedChecks = await runChecks(); + expect(failedChecks, isEmpty); + }); + }, timeout: Timeout(Duration(minutes: 2))); +} diff --git a/pkg/linter/test/verify_machine_json_test.dart b/pkg/linter/test/verify_machine_json_test.dart new file mode 100644 index 00000000000..1451edbcf07 --- /dev/null +++ b/pkg/linter/test/verify_machine_json_test.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +import '../tool/machine.dart'; +import 'util/test_utils.dart'; + +void main() { + group('machine output tests', () { + setUp(setUpSharedTestEnvironment); + test("ensure 'rules.json' is up to date", () async { + var rulesFile = machineJsonFile(); + var onDisk = rulesFile.readAsStringSync(); + var generated = await generateRulesJson(); + expect( + generated, + onDisk, + reason: "'rules.json' is out of date. Regenerate by running " + '`dart tool/machine.dart -w`', + ); + }); + }); +} diff --git a/pkg/linter/test/verify_reflective_test_suites.dart b/pkg/linter/test/verify_reflective_test_suites.dart new file mode 100644 index 00000000000..30906f1710e --- /dev/null +++ b/pkg/linter/test/verify_reflective_test_suites.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'test_constants.dart'; + +void main() { + group('check reflective test suites', () { + group('rules', () { + var testDirPath = + PhysicalResourceProvider.INSTANCE.pathContext.absolute(ruleTestDir); + _VerifyTests(testDirPath).build(); + }); + }); +} + +/// Helper class to test that [testAllFileName] files are properly set up. +/// (Cribbed from `analyzer_utilities`.) +class _VerifyTests { + final String testDirPath; + + _VerifyTests(this.testDirPath); + + String get testAllFileName => 'all.dart'; + + /// Build tests. + void build() { + var provider = PhysicalResourceProvider.INSTANCE; + var collection = AnalysisContextCollection( + resourceProvider: provider, includedPaths: [testDirPath]); + var contexts = collection.contexts; + if (contexts.length != 1) { + fail('The test directory contains multiple analysis contexts.'); + } + + _buildTestsIn(contexts.first.currentSession, testDirPath, + provider.getFolder(testDirPath)); + } + + void _buildTestsIn( + AnalysisSession session, String testDirPath, Folder directory) { + var testFileNames = []; + File? testAllFile; + var children = directory.getChildren(); + children + .sort((first, second) => first.shortName.compareTo(second.shortName)); + for (var child in children) { + if (child is Folder) { + if (child.getChildAssumingFile(testAllFileName).exists) { + testFileNames.add('${child.shortName}/$testAllFileName'); + } + _buildTestsIn(session, testDirPath, child); + } else if (child is File) { + var name = child.shortName; + if (name == testAllFileName) { + testAllFile = child; + } else if (name.endsWith('_test.dart')) { + testFileNames.add(name); + } + } + } + var relativePath = path.relative(directory.path, from: testDirPath); + test(relativePath, () async { + if (testFileNames.isEmpty) return; + if (testAllFile == null) return; + + var result = session.getParsedUnit(testAllFile.path); + if (result is! ParsedUnitResult) { + fail('Could not parse ${testAllFile.path}'); + } + var importedFiles = []; + for (var directive in result.unit.directives) { + if (directive is ImportDirective) { + var uri = directive.uri.stringValue; + if (uri == null) { + fail('Invalid URI: $directive'); + } + importedFiles.add(uri); + } + } + var missingFiles = []; + for (var testFileName in testFileNames) { + if (!importedFiles.contains(testFileName)) { + missingFiles.add(testFileName); + } + } + if (missingFiles.isNotEmpty) { + fail( + 'Tests missing from "$testDirPath/$testAllFileName": ${missingFiles.join(', ')}'); + } + var extraImports = []; + for (var importedFile in importedFiles) { + if (!testFileNames.contains(importedFile)) { + extraImports.add(importedFile); + } + } + if (extraImports.isNotEmpty) { + fail( + 'Extra tests in "$testDirPath/$testAllFileName": ${extraImports.join(', ')}'); + } + }); + } +} diff --git a/pkg/linter/test_data/analysis_options.yaml b/pkg/linter/test_data/analysis_options.yaml new file mode 100644 index 00000000000..13e5d8f848b --- /dev/null +++ b/pkg/linter/test_data/analysis_options.yaml @@ -0,0 +1 @@ +# We need this file to prevent using the one from the package root. diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart new file mode 100644 index 00000000000..21576d30681 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart @@ -0,0 +1,3 @@ +import 'dart:html'; //OK +import 'dart:js'; //OK +import 'dart:js_util'; //OK diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart new file mode 100644 index 00000000000..21576d30681 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart @@ -0,0 +1,3 @@ +import 'dart:html'; //OK +import 'dart:js'; //OK +import 'dart:js_util'; //OK diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml new file mode 100644 index 00000000000..6666d30e12d --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml @@ -0,0 +1,5 @@ +name: non_flutter_app +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart new file mode 100644 index 00000000000..4e68cc2a332 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart @@ -0,0 +1,3 @@ +import 'dart:html'; //LINT +import 'dart:js'; //LINT +import 'dart:js_util'; //LINT diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart new file mode 100644 index 00000000000..fa12ba8d43e --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart @@ -0,0 +1 @@ +import 'dart:math'; //OK diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml new file mode 100644 index 00000000000..2d63f912606 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml @@ -0,0 +1,14 @@ +name: non_web_app +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/lib/main.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/lib/main.dart new file mode 100644 index 00000000000..21576d30681 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/lib/main.dart @@ -0,0 +1,3 @@ +import 'dart:html'; //OK +import 'dart:js'; //OK +import 'dart:js_util'; //OK diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/pubspec.yaml b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/pubspec.yaml new file mode 100644 index 00000000000..fe0dbb943ee --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/pubspec.yaml @@ -0,0 +1,17 @@ +name: sample_project +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/web/README b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/web/README new file mode 100644 index 00000000000..b36eb374b30 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_app/web/README @@ -0,0 +1 @@ +placeholder. diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart new file mode 100644 index 00000000000..21576d30681 --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart @@ -0,0 +1,3 @@ +import 'dart:html'; //OK +import 'dart:js'; //OK +import 'dart:js_util'; //OK diff --git a/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml new file mode 100644 index 00000000000..8a3a36c4aff --- /dev/null +++ b/pkg/linter/test_data/integration/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml @@ -0,0 +1,21 @@ +name: sample_project +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + web: + pluginClass: SamplePlugin + fileName: main.dart diff --git a/pkg/linter/test_data/integration/close_sinks/src/a.dart b/pkg/linter/test_data/integration/close_sinks/src/a.dart new file mode 100644 index 00000000000..86335ed8213 --- /dev/null +++ b/pkg/linter/test_data/integration/close_sinks/src/a.dart @@ -0,0 +1,159 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// @dart=2.9 + +// ignore_for_file: prefer_initializing_formals +// ignore_for_file: unnecessary_new +// ignore_for_file: unnecessary_this +// ignore_for_file: unused_field +// ignore_for_file: unused_local_variable +// ignore_for_file: use_setters_to_change_properties + +import 'dart:async'; +import 'dart:io'; + +class B0 { + IOSink _sinkB0; // OK + void init(filename) { + _sinkB0 = new File(filename).openWrite(); + } + + void dispose(filename) { + this._sinkB0.close(); + } +} + +class MockIOSink implements Sink { + @override + void add(data) {} + + @override + void close() {} +} + +IOSink outSink = stdout; +void inScope() { + IOSink currentOut = outSink; +} + +class A { + IOSink _sinkA; // LINT + void init(String filename) { + _sinkA = new File(filename).openWrite(); + } +} + +class B { + IOSink _sinkB; + void init(String filename) { + _sinkB = new File(filename).openWrite(); // OK + } + + void dispose(filename) { + _sinkB.close(); + } +} + +class B1 { + Socket _socketB1; + Future init(String filename) async { + _socketB1 = await Socket.connect(null /*address*/, 1234); // OK + } + + void dispose(String filename) { + _socketB1.destroy(); + } +} + +class C { + final IOSink _sinkC; // OK + + C(this._sinkC); +} + +class C1 { + final IOSink _sinkC1; // OK + final Object unrelated; + + C1.initializer(IOSink sink, blah) + : this._sinkC1 = sink, + this.unrelated = blah; +} + +class C2 { + IOSink _sinkC2; // OK + + void initialize(IOSink sink) { + this._sinkC2 = sink; + } +} + +class C3 { + IOSink _sinkC3; // OK + + void initialize(IOSink sink) { + _sinkC3 = sink; + } +} + +class D { + IOSink init(String filename) { + IOSink _sinkF = new File(filename).openWrite(); // OK + return _sinkF; + } +} + +void someFunction() { + IOSink _sinkSomeFunction; // LINT +} + +void someFunctionOK() { + IOSink _sinkFOK; // OK + _sinkFOK.close(); +} + +IOSink someFunctionReturningIOSink() { + IOSink _sinkF = new File('filename').openWrite(); // OK + return _sinkF; +} + +void startChunkedConversion(Socket sink) { + Sink stringSink; + if (sink is IOSink) { + stringSink = sink; + } else { + stringSink = new MockIOSink(); + } +} + +void onListen(Stream stream) { + StreamController controllerListen = new StreamController(); + stream.listen((int event) { + event.toString(); + }, onError: controllerListen.addError, onDone: controllerListen.close); +} + +void someFunctionClosing(StreamController controller) {} + +void controllerPassedAsArgument() { + StreamController controllerArgument = new StreamController(); + someFunctionClosing(controllerArgument); +} + +void fluentInvocation() { + StreamController cascadeController = new StreamController() + ..add(null) + ..close(); +} + +class CascadeSink { + StreamController cascadeController = new StreamController(); // OK + + void closeSink() { + cascadeController + ..add(null) + ..close(); + } +} diff --git a/pkg/linter/test_data/integration/p2/_pubspec.yaml b/pkg/linter/test_data/integration/p2/_pubspec.yaml new file mode 100644 index 00000000000..cef9aff8d4a --- /dev/null +++ b/pkg/linter/test_data/integration/p2/_pubspec.yaml @@ -0,0 +1 @@ +name: p2 \ No newline at end of file diff --git a/pkg/linter/test_data/integration/p2/lintconfig.yaml b/pkg/linter/test_data/integration/p2/lintconfig.yaml new file mode 100644 index 00000000000..7ceba2a41ab --- /dev/null +++ b/pkg/linter/test_data/integration/p2/lintconfig.yaml @@ -0,0 +1,2 @@ +files: + exclude: test_data/integration/p2/src/ignored/** \ No newline at end of file diff --git a/pkg/linter/test_data/integration/p2/lintconfig2.yaml b/pkg/linter/test_data/integration/p2/lintconfig2.yaml new file mode 100644 index 00000000000..70d1a4245dd --- /dev/null +++ b/pkg/linter/test_data/integration/p2/lintconfig2.yaml @@ -0,0 +1,3 @@ +rules: + style_guide: + camel_case_types: false \ No newline at end of file diff --git a/pkg/linter/test_data/integration/p2/src/a.dart b/pkg/linter/test_data/integration/p2/src/a.dart new file mode 100644 index 00000000000..0d86395973e --- /dev/null +++ b/pkg/linter/test_data/integration/p2/src/a.dart @@ -0,0 +1,2 @@ +/// Test class. +class a {} diff --git a/pkg/linter/test_data/integration/p2/src/ignored/b.dart b/pkg/linter/test_data/integration/p2/src/ignored/b.dart new file mode 100644 index 00000000000..4d7e4b51f02 --- /dev/null +++ b/pkg/linter/test_data/integration/p2/src/ignored/b.dart @@ -0,0 +1,2 @@ +/// Class b. +class b {} diff --git a/pkg/linter/test_data/integration/p2/src/ignored/c.dart b/pkg/linter/test_data/integration/p2/src/ignored/c.dart new file mode 100644 index 00000000000..356050302c4 --- /dev/null +++ b/pkg/linter/test_data/integration/p2/src/ignored/c.dart @@ -0,0 +1,2 @@ +/// Class c. +class c {} diff --git a/pkg/linter/test_data/mock_packages/fixnum/lib/fixnum.dart b/pkg/linter/test_data/mock_packages/fixnum/lib/fixnum.dart new file mode 100644 index 00000000000..7b41c957ad8 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/fixnum/lib/fixnum.dart @@ -0,0 +1,5 @@ +library fixnum; + +class Int32 {} + +class Int64 {} diff --git a/pkg/linter/test_data/mock_packages/fixnum/pubspec.yaml b/pkg/linter/test_data/mock_packages/fixnum/pubspec.yaml new file mode 100644 index 00000000000..3c5f856f3d1 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/fixnum/pubspec.yaml @@ -0,0 +1,4 @@ +name: fixnum + +environment: + sdk: '>=2.2.2 <3.0.0' diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/foundation.dart b/pkg/linter/test_data/mock_packages/flutter/lib/foundation.dart new file mode 100644 index 00000000000..a3b694c4255 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/foundation.dart @@ -0,0 +1,17 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//export 'package:meta/meta.dart' +// show +// immutable, +// mustCallSuper, +// optionalTypeArgs, +// protected, +// required, +// visibleForTesting; + +export 'src/foundation/constants.dart'; +export 'src/foundation/diagnostics.dart'; +export 'src/foundation/key.dart'; +export 'src/foundation/platform.dart'; diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/material.dart b/pkg/linter/test_data/mock_packages/flutter/lib/material.dart new file mode 100644 index 00000000000..674005eda9f --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/material.dart @@ -0,0 +1,7 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/material/button.dart'; +export 'src/material/scaffold.dart'; +export 'widgets.dart'; diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/painting.dart b/pkg/linter/test_data/mock_packages/flutter/lib/painting.dart new file mode 100644 index 00000000000..58158555b82 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/painting.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'src/painting/box_decoration.dart'; diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/rendering.dart b/pkg/linter/test_data/mock_packages/flutter/lib/rendering.dart new file mode 100644 index 00000000000..b2b89c6268d --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/rendering.dart @@ -0,0 +1,5 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/rendering/flex.dart'; diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/constants.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/constants.dart new file mode 100644 index 00000000000..9ea62f8ac74 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/constants.dart @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const bool kDebugMode = true; \ No newline at end of file diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/diagnostics.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/diagnostics.dart new file mode 100644 index 00000000000..f51052cf30a --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/diagnostics.dart @@ -0,0 +1,3736 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:meta/meta.dart'; + +import 'assertions.dart'; +import 'constants.dart'; +import 'debug.dart'; + +// Examples can assume: +// int rows, columns; +// String _name; +// bool inherit; + +/// The various priority levels used to filter which diagnostics are shown and +/// omitted. +/// +/// Trees of Flutter diagnostics can be very large so filtering the diagnostics +/// shown matters. Typically filtering to only show diagnostics with at least +/// level [debug] is appropriate. +enum DiagnosticLevel { + /// Diagnostics that should not be shown. + /// + /// If a user chooses to display [hidden] diagnostics, they should not expect + /// the diagnostics to be formatted consistently with other diagnostics and + /// they should expect them to sometimes be misleading. For example, + /// [FlagProperty] and [ObjectFlagProperty] have uglier formatting when the + /// property `value` does does not match a value with a custom flag + /// description. An example of a misleading diagnostic is a diagnostic for + /// a property that has no effect because some other property of the object is + /// set in a way that causes the hidden property to have no effect. + hidden, + + /// A diagnostic that is likely to be low value but where the diagnostic + /// display is just as high quality as a diagnostic with a higher level. + /// + /// Use this level for diagnostic properties that match their default value + /// and other cases where showing a diagnostic would not add much value such + /// as an [IterableProperty] where the value is empty. + fine, + + /// Diagnostics that should only be shown when performing fine grained + /// debugging of an object. + /// + /// Unlike a [fine] diagnostic, these diagnostics provide important + /// information about the object that is likely to be needed to debug. Used by + /// properties that are important but where the property value is too verbose + /// (e.g. 300+ characters long) to show with a higher diagnostic level. + debug, + + /// Interesting diagnostics that should be typically shown. + info, + + /// Very important diagnostics that indicate problematic property values. + /// + /// For example, use if you would write the property description + /// message in ALL CAPS. + warning, + + /// Diagnostics that provide a hint about best practices. + /// + /// For example, a diagnostic describing best practices for fixing an error. + hint, + + /// Diagnostics that summarize other diagnostics present. + /// + /// For example, use this level for a short one or two line summary + /// describing other diagnostics present. + summary, + + /// Diagnostics that indicate errors or unexpected conditions. + /// + /// For example, use for property values where computing the value throws an + /// exception. + error, + + /// Special level indicating that no diagnostics should be shown. + /// + /// Do not specify this level for diagnostics. This level is only used to + /// filter which diagnostics are shown. + off, +} + +/// Styles for displaying a node in a [DiagnosticsNode] tree. +/// +/// See also: +/// +/// * [DiagnosticsNode.toStringDeep], which dumps text art trees for these +/// styles. +enum DiagnosticsTreeStyle { + /// A style that does not display the tree, for release mode. + none, + + /// Sparse style for displaying trees. + /// + /// See also: + /// + /// * [RenderObject], which uses this style. + sparse, + + /// Connects a node to its parent with a dashed line. + /// + /// See also: + /// + /// * [RenderSliverMultiBoxAdaptor], which uses this style to distinguish + /// offstage children from onstage children. + offstage, + + /// Slightly more compact version of the [sparse] style. + /// + /// See also: + /// + /// * [Element], which uses this style. + dense, + + /// Style that enables transitioning from nodes of one style to children of + /// another. + /// + /// See also: + /// + /// * [RenderParagraph], which uses this style to display a [TextSpan] child + /// in a way that is compatible with the [DiagnosticsTreeStyle.sparse] + /// style of the [RenderObject] tree. + transition, + + /// Style for displaying content describing an error. + /// + /// See also: + /// + /// * [FlutterError], which uses this style for the root node in a tree + /// describing an error. + error, + + /// Render the tree just using whitespace without connecting parents to + /// children using lines. + /// + /// See also: + /// + /// * [SliverGeometry], which uses this style. + whitespace, + + /// Render the tree without indenting children at all. + /// + /// See also: + /// + /// * [DiagnosticsStackTrace], which uses this style. + flat, + + /// Render the tree on a single line without showing children. + singleLine, + + /// Render the tree using a style appropriate for properties that are part + /// of an error message. + /// + /// The name is placed on one line with the value and properties placed on + /// the following line. + /// + /// See also: + /// * [singleLine], which displays the same information but keeps the + /// property and value on the same line. + errorProperty, + + /// Render only the immediate properties of a node instead of the full tree. + /// + /// See also: + /// + /// * [DebugOverflowIndicator], which uses this style to display just the + /// immediate children of a node. + shallow, + + /// Render only the children of a node truncating before the tree becomes too + /// large. + truncateChildren, +} + +/// Configuration specifying how a particular [DiagnosticsTreeStyle] should be +/// rendered as text art. +/// +/// See also: +/// +/// * [sparseTextConfiguration], which is a typical style. +/// * [transitionTextConfiguration], which is an example of a complex tree style. +/// * [DiagnosticsNode.toStringDeep], for code using [TextTreeConfiguration] +/// to render text art for arbitrary trees of [DiagnosticsNode] objects. +class TextTreeConfiguration { + /// Create a configuration object describing how to render a tree as text. + /// + /// All of the arguments must not be null. + TextTreeConfiguration({ + @required this.prefixLineOne, + @required this.prefixOtherLines, + @required this.prefixLastChildLineOne, + @required this.prefixOtherLinesRootNode, + @required this.linkCharacter, + @required this.propertyPrefixIfChildren, + @required this.propertyPrefixNoChildren, + this.lineBreak = '\n', + this.lineBreakProperties = true, + this.afterName = ':', + this.afterDescriptionIfBody = '', + this.afterDescription = '', + this.beforeProperties = '', + this.afterProperties = '', + this.mandatoryAfterProperties = '', + this.propertySeparator = '', + this.bodyIndent = '', + this.footer = '', + this.showChildren = true, + this.addBlankLineIfNoChildren = true, + this.isNameOnOwnLine = false, + this.isBlankLineBetweenPropertiesAndChildren = true, + this.beforeName = '', + this.suffixLineOne = '', + this.manditoryFooter = '', + }) : assert(prefixLineOne != null), + assert(prefixOtherLines != null), + assert(prefixLastChildLineOne != null), + assert(prefixOtherLinesRootNode != null), + assert(linkCharacter != null), + assert(propertyPrefixIfChildren != null), + assert(propertyPrefixNoChildren != null), + assert(lineBreak != null), + assert(lineBreakProperties != null), + assert(afterName != null), + assert(afterDescriptionIfBody != null), + assert(afterDescription != null), + assert(beforeProperties != null), + assert(afterProperties != null), + assert(propertySeparator != null), + assert(bodyIndent != null), + assert(footer != null), + assert(showChildren != null), + assert(addBlankLineIfNoChildren != null), + assert(isNameOnOwnLine != null), + assert(isBlankLineBetweenPropertiesAndChildren != null), + childLinkSpace = ' ' * linkCharacter.length; + + /// Prefix to add to the first line to display a child with this style. + final String prefixLineOne; + + /// Suffix to add to end of the first line to make its length match the footer. + final String suffixLineOne; + + /// Prefix to add to other lines to display a child with this style. + /// + /// [prefixOtherLines] should typically be one character shorter than + /// [prefixLineOne] as + final String prefixOtherLines; + + /// Prefix to add to the first line to display the last child of a node with + /// this style. + final String prefixLastChildLineOne; + + /// Additional prefix to add to other lines of a node if this is the root node + /// of the tree. + final String prefixOtherLinesRootNode; + + /// Prefix to add before each property if the node as children. + /// + /// Plays a similar role to [linkCharacter] except that some configurations + /// intentionally use a different line style than the [linkCharacter]. + final String propertyPrefixIfChildren; + + /// Prefix to add before each property if the node does not have children. + /// + /// This string is typically a whitespace string the same length as + /// [propertyPrefixIfChildren] but can have a different length. + final String propertyPrefixNoChildren; + + /// Character to use to draw line linking parent to child. + /// + /// The first child does not require a line but all subsequent children do + /// with the line drawn immediately before the left edge of the previous + /// sibling. + final String linkCharacter; + + /// Whitespace to draw instead of the childLink character if this node is the + /// last child of its parent so no link line is required. + final String childLinkSpace; + + /// Character(s) to use to separate lines. + /// + /// Typically leave set at the default value of '\n' unless this style needs + /// to treat lines differently as is the case for + /// [singleLineTextConfiguration]. + final String lineBreak; + + /// Whether to place line breaks between properties or to leave all + /// properties on one line. + final bool lineBreakProperties; + + /// Text added immediately before the name of the node. + /// + /// See [errorTextConfiguration] for an example of using this to achieve a + /// custom line art style. + final String beforeName; + + /// Text added immediately after the name of the node. + /// + /// See [transitionTextConfiguration] for an example of using a value other + /// than ':' to achieve a custom line art style. + final String afterName; + + /// Text to add immediately after the description line of a node with + /// properties and/or children if the node has a body. + final String afterDescriptionIfBody; + + /// Text to add immediately after the description line of a node with + /// properties and/or children. + final String afterDescription; + + /// Optional string to add before the properties of a node. + /// + /// Only displayed if the node has properties. + /// See [singleLineTextConfiguration] for an example of using this field + /// to enclose the property list with parenthesis. + final String beforeProperties; + + /// Optional string to add after the properties of a node. + /// + /// See documentation for [beforeProperties]. + final String afterProperties; + + /// Mandatory string to add after the properties of a node regardless of + /// whether the node has any properties. + /// + /// See [headerLineTextConfiguration] for an example of using this field to + /// add a colon at the end of the header line. + final String mandatoryAfterProperties; + + /// Property separator to add between properties. + /// + /// See [singleLineTextConfiguration] for an example of using this field + /// to render properties as a comma separated list. + final String propertySeparator; + + /// Prefix to add to all lines of the body of the tree node. + /// + /// The body is all content in the node other than the name and description. + final String bodyIndent; + + /// Whether the children of a node should be shown. + /// + /// See [singleLineTextConfiguration] for an example of using this field to + /// hide all children of a node. + final bool showChildren; + + /// Whether to add a blank line at the end of the output for a node if it has + /// no children. + /// + /// See [denseTextConfiguration] for an example of setting this to false. + final bool addBlankLineIfNoChildren; + + /// Whether the name should be displayed on the same line as the description. + final bool isNameOnOwnLine; + + /// Footer to add as its own line at the end of a non-root node. + /// + /// See [transitionTextConfiguration] for an example of using footer to draw a box + /// around the node. [footer] is indented the same amount as [prefixOtherLines]. + final String footer; + + /// Footer to add even for root nodes. + final String manditoryFooter; + + /// Add a blank line between properties and children if both are present. + final bool isBlankLineBetweenPropertiesAndChildren; +} + +/// Default text tree configuration. +/// +/// Example: +/// ``` +/// : +/// │ +/// │ +/// │ ... +/// │ +/// ├─: +/// │ │ +/// │ │ +/// │ │ ... +/// │ │ +/// │ │ +/// │ └─: +/// │ +/// │ +/// │ ... +/// │ +/// │ +/// └─: ' +/// +/// +/// ... +/// +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.sparse], uses this style for ASCII art display. +final TextTreeConfiguration sparseTextConfiguration = TextTreeConfiguration( + prefixLineOne: '├─', + prefixOtherLines: ' ', + prefixLastChildLineOne: '└─', + linkCharacter: '│', + propertyPrefixIfChildren: '│ ', + propertyPrefixNoChildren: ' ', + prefixOtherLinesRootNode: ' ', +); + +/// Identical to [sparseTextConfiguration] except that the lines connecting +/// parent to children are dashed. +/// +/// Example: +/// ``` +/// : +/// │ +/// │ +/// │ ... +/// │ +/// ├─: +/// ╎ │ +/// ╎ │ +/// ╎ │ ... +/// ╎ │ +/// ╎ │ +/// ╎ └─: +/// ╎ +/// ╎ +/// ╎ ... +/// ╎ +/// ╎ +/// ╎╌: +/// ╎ │ +/// ╎ │ +/// ╎ │ ... +/// ╎ │ +/// ╎ │ +/// ╎ └─: +/// ╎ +/// ╎ +/// ╎ ... +/// ╎ +/// ╎ +/// └╌: ' +/// +/// +/// ... +/// +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.offstage], uses this style for ASCII art display. +final TextTreeConfiguration dashedTextConfiguration = TextTreeConfiguration( + prefixLineOne: '╎╌', + prefixLastChildLineOne: '└╌', + prefixOtherLines: ' ', + linkCharacter: '╎', + // Intentionally not set as a dashed line as that would make the properties + // look like they were disabled. + propertyPrefixIfChildren: '│ ', + propertyPrefixNoChildren: ' ', + prefixOtherLinesRootNode: ' ', +); + +/// Dense text tree configuration that minimizes horizontal whitespace. +/// +/// Example: +/// ``` +/// : (; ) +/// ├: (, , ) +/// └: (, , ) +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.dense], uses this style for ASCII art display. +final TextTreeConfiguration denseTextConfiguration = TextTreeConfiguration( + propertySeparator: ', ', + beforeProperties: '(', + afterProperties: ')', + lineBreakProperties: false, + prefixLineOne: '├', + prefixOtherLines: '', + prefixLastChildLineOne: '└', + linkCharacter: '│', + propertyPrefixIfChildren: '│', + propertyPrefixNoChildren: ' ', + prefixOtherLinesRootNode: '', + addBlankLineIfNoChildren: false, + isBlankLineBetweenPropertiesAndChildren: false, +); + +/// Configuration that draws a box around a leaf node. +/// +/// Used by leaf nodes such as [TextSpan] to draw a clear border around the +/// contents of a node. +/// +/// Example: +/// ``` +/// +/// ╞═╦══ ═══ +/// │ ║ : +/// │ ║ +/// │ ║ ... +/// │ ╚═══════════ +/// ╘═╦══ ═══ +/// ║ : +/// ║ +/// ║ ... +/// ╚═══════════ +/// ``` +/// +/// /// See also: +/// +/// * [DiagnosticsTreeStyle.transition], uses this style for ASCII art display. +final TextTreeConfiguration transitionTextConfiguration = TextTreeConfiguration( + prefixLineOne: '╞═╦══ ', + prefixLastChildLineOne: '╘═╦══ ', + prefixOtherLines: ' ║ ', + footer: ' ╚═══════════', + linkCharacter: '│', + // Subtree boundaries are clear due to the border around the node so omit the + // property prefix. + propertyPrefixIfChildren: '', + propertyPrefixNoChildren: '', + prefixOtherLinesRootNode: '', + afterName: ' ═══', + // Add a colon after the description if the node has a body to make the + // connection between the description and the body clearer. + afterDescriptionIfBody: ':', + // Members are indented an extra two spaces to disambiguate as the description + // is placed within the box instead of along side the name as is the case for + // other styles. + bodyIndent: ' ', + isNameOnOwnLine: true, + // No need to add a blank line as the footer makes the boundary of this + // subtree unambiguous. + addBlankLineIfNoChildren: false, + isBlankLineBetweenPropertiesAndChildren: false, +); + +/// Configuration that draws a box around a node ignoring the connection to the +/// parents. +/// +/// If nested in a tree, this node is best displayed in the property box rather +/// than as a traditional child. +/// +/// Used to draw a decorative box around detailed descriptions of an exception. +/// +/// Example: +/// ``` +/// ══╡ : ╞═════════════════════════════════════ +/// +/// ... +/// ├─: +/// ╎ │ +/// ╎ │ +/// ╎ │ ... +/// ╎ │ +/// ╎ │ +/// ╎ └─: +/// ╎ +/// ╎ +/// ╎ ... +/// ╎ +/// ╎ +/// ╎╌: +/// ╎ │ +/// ╎ │ +/// ╎ │ ... +/// ╎ │ +/// ╎ │ +/// ╎ └─: +/// ╎ +/// ╎ +/// ╎ ... +/// ╎ +/// ╎ +/// └╌: ' +/// ════════════════════════════════════════════════════════════════ +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.error], uses this style for ASCII art display. +final TextTreeConfiguration errorTextConfiguration = TextTreeConfiguration( + prefixLineOne: '╞═╦', + prefixLastChildLineOne: '╘═╦', + prefixOtherLines: ' ║ ', + footer: ' ╚═══════════', + linkCharacter: '│', + // Subtree boundaries are clear due to the border around the node so omit the + // property prefix. + propertyPrefixIfChildren: '', + propertyPrefixNoChildren: '', + prefixOtherLinesRootNode: '', + beforeName: '══╡ ', + suffixLineOne: ' ╞══', + manditoryFooter: '═════', + // No need to add a blank line as the footer makes the boundary of this + // subtree unambiguous. + addBlankLineIfNoChildren: false, + isBlankLineBetweenPropertiesAndChildren: false, +); + +/// Whitespace only configuration where children are consistently indented +/// two spaces. +/// +/// Use this style for displaying properties with structured values or for +/// displaying children within a [transitionTextConfiguration] as using a style that +/// draws line art would be visually distracting for those cases. +/// +/// Example: +/// ``` +/// +/// : : +/// +/// +/// : : +/// +/// +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.whitespace], uses this style for ASCII art display. +final TextTreeConfiguration whitespaceTextConfiguration = TextTreeConfiguration( + prefixLineOne: '', + prefixLastChildLineOne: '', + prefixOtherLines: ' ', + prefixOtherLinesRootNode: ' ', + bodyIndent: '', + propertyPrefixIfChildren: '', + propertyPrefixNoChildren: '', + linkCharacter: ' ', + addBlankLineIfNoChildren: false, + // Add a colon after the description and before the properties to link the + // properties to the description line. + afterDescriptionIfBody: ':', + isBlankLineBetweenPropertiesAndChildren: false, +); + +/// Whitespace only configuration where children are not indented. +/// +/// Use this style when indentation is not needed to disambiguate parents from +/// children as in the case of a [DiagnosticsStackTrace]. +/// +/// Example: +/// ``` +/// +/// : : +/// +/// +/// : : +/// +/// +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.flat], uses this style for ASCII art display. +final TextTreeConfiguration flatTextConfiguration = TextTreeConfiguration( + prefixLineOne: '', + prefixLastChildLineOne: '', + prefixOtherLines: '', + prefixOtherLinesRootNode: '', + bodyIndent: '', + propertyPrefixIfChildren: '', + propertyPrefixNoChildren: '', + linkCharacter: '', + addBlankLineIfNoChildren: false, + // Add a colon after the description and before the properties to link the + // properties to the description line. + afterDescriptionIfBody: ':', + isBlankLineBetweenPropertiesAndChildren: false, +); + +/// Render a node as a single line omitting children. +/// +/// Example: +/// `: (, , ..., )` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.singleLine], uses this style for ASCII art display. +final TextTreeConfiguration singleLineTextConfiguration = TextTreeConfiguration( + propertySeparator: ', ', + beforeProperties: '(', + afterProperties: ')', + prefixLineOne: '', + prefixOtherLines: '', + prefixLastChildLineOne: '', + lineBreak: '', + lineBreakProperties: false, + addBlankLineIfNoChildren: false, + showChildren: false, + propertyPrefixIfChildren: ' ', + propertyPrefixNoChildren: ' ', + linkCharacter: '', + prefixOtherLinesRootNode: '', +); + +/// Render the name on a line followed by the body and properties on the next +/// line omitting the children. +/// +/// Example: +/// ``` +/// : +/// (, , ..., ) +/// ``` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.errorProperty], uses this style for ASCII art +/// display. +final TextTreeConfiguration errorPropertyTextConfiguration = + TextTreeConfiguration( + propertySeparator: ', ', + beforeProperties: '(', + afterProperties: ')', + prefixLineOne: '', + prefixOtherLines: '', + prefixLastChildLineOne: '', + lineBreak: '\n', + lineBreakProperties: false, + addBlankLineIfNoChildren: false, + showChildren: false, + propertyPrefixIfChildren: ' ', + propertyPrefixNoChildren: ' ', + linkCharacter: '', + prefixOtherLinesRootNode: '', + afterName: ':', + isNameOnOwnLine: true, +); + +/// Render a node on multiple lines omitting children. +/// +/// Example: +/// `: +/// +/// +/// ` +/// +/// See also: +/// +/// * [DiagnosticsTreeStyle.shallow] +final TextTreeConfiguration shallowTextConfiguration = TextTreeConfiguration( + prefixLineOne: '', + prefixLastChildLineOne: '', + prefixOtherLines: ' ', + prefixOtherLinesRootNode: ' ', + bodyIndent: '', + propertyPrefixIfChildren: '', + propertyPrefixNoChildren: '', + linkCharacter: ' ', + addBlankLineIfNoChildren: false, + // Add a colon after the description and before the properties to link the + // properties to the description line. + afterDescriptionIfBody: ':', + isBlankLineBetweenPropertiesAndChildren: false, + showChildren: false, +); + +enum _WordWrapParseMode { inSpace, inWord, atBreak } + +/// Builder that builds a String with specified prefixes for the first and +/// subsequent lines. +/// +/// Allows for the incremental building of strings using `write*()` methods. +/// The strings are concatenated into a single string with the first line +/// prefixed by [prefixLineOne] and subsequent lines prefixed by +/// [prefixOtherLines]. +class _PrefixedStringBuilder { + _PrefixedStringBuilder( + {@required this.prefixLineOne, + @required String prefixOtherLines, + this.wrapWidth}) + : _prefixOtherLines = prefixOtherLines; + + /// Prefix to add to the first line. + final String prefixLineOne; + + /// Prefix to add to subsequent lines. + /// + /// The prefix can be modified while the string is being built in which case + /// subsequent lines will be added with the modified prefix. + String get prefixOtherLines => _nextPrefixOtherLines ?? _prefixOtherLines; + String _prefixOtherLines; + set prefixOtherLines(String prefix) { + _prefixOtherLines = prefix; + _nextPrefixOtherLines = null; + } + + String _nextPrefixOtherLines; + void incrementPrefixOtherLines(String suffix, + {@required bool updateCurrentLine}) { + if (_currentLine.isEmpty || updateCurrentLine) { + _prefixOtherLines = prefixOtherLines + suffix; + _nextPrefixOtherLines = null; + } else { + _nextPrefixOtherLines = prefixOtherLines + suffix; + } + } + + final int wrapWidth; + + /// Buffer containing lines that have already been completely laid out. + final StringBuffer _buffer = StringBuffer(); + + /// Buffer containing the current line that has not yet been wrapped. + final StringBuffer _currentLine = StringBuffer(); + + /// List of pairs of integers indicating the start and end of each block of + /// text within _currentLine that can be wrapped. + final List _wrappableRanges = []; + + /// Whether the string being built already has more than 1 line. + bool get requiresMultipleLines => + _numLines > 1 || + (_numLines == 1 && _currentLine.isNotEmpty) || + (_currentLine.length + _getCurrentPrefix(true).length > wrapWidth); + + bool get isCurrentLineEmpty => _currentLine.isEmpty; + + int _numLines = 0; + + void _finalizeLine(bool addTrailingLineBreak) { + final bool firstLine = _buffer.isEmpty; + final String text = _currentLine.toString(); + _currentLine.clear(); + + if (_wrappableRanges.isEmpty) { + // Fast path. There were no wrappable spans of text. + _writeLine( + text, + includeLineBreak: addTrailingLineBreak, + firstLine: firstLine, + ); + return; + } + final Iterable lines = _wordWrapLine( + text, + _wrappableRanges, + wrapWidth, + startOffset: firstLine ? prefixLineOne.length : _prefixOtherLines.length, + otherLineOffset: + firstLine ? _prefixOtherLines.length : _prefixOtherLines.length, + ); + int i = 0; + final int length = lines.length; + for (String line in lines) { + i++; + _writeLine( + line, + includeLineBreak: addTrailingLineBreak || i < length, + firstLine: firstLine, + ); + } + _wrappableRanges.clear(); + } + + /// Wraps the given string at the given width. + /// + /// Wrapping occurs at space characters (U+0020). + /// + /// This is not suitable for use with arbitrary Unicode text. For example, it + /// doesn't implement UAX #14, can't handle ideographic text, doesn't hyphenate, + /// and so forth. It is only intended for formatting error messages. + /// + /// This method wraps a sequence of text where only some spans of text can be + /// used as wrap boundaries. + static Iterable _wordWrapLine( + String message, List wrapRanges, int width, + {int startOffset = 0, int otherLineOffset = 0}) sync* { + if (message.length + startOffset < width) { + // Nothing to do. The line doesn't wrap. + yield message; + return; + } + int startForLengthCalculations = -startOffset; + bool addPrefix = false; + int index = 0; + _WordWrapParseMode mode = _WordWrapParseMode.inSpace; + int lastWordStart; + int lastWordEnd; + int start = 0; + + int currentChunk = 0; + + // This helper is called with increasing indexes. + bool noWrap(int index) { + while (true) { + if (currentChunk >= wrapRanges.length) return true; + + if (index < wrapRanges[currentChunk + 1]) break; // Found nearest chunk. + currentChunk += 2; + } + return index < wrapRanges[currentChunk]; + } + + while (true) { + switch (mode) { + case _WordWrapParseMode + .inSpace: // at start of break point (or start of line); can't break until next break + while ((index < message.length) && (message[index] == ' ')) + index += 1; + lastWordStart = index; + mode = _WordWrapParseMode.inWord; + break; + case _WordWrapParseMode + .inWord: // looking for a good break point. Treat all text + while ((index < message.length) && + (message[index] != ' ' || noWrap(index))) index += 1; + mode = _WordWrapParseMode.atBreak; + break; + case _WordWrapParseMode.atBreak: // at start of break point + if ((index - startForLengthCalculations > width) || + (index == message.length)) { + // we are over the width line, so break + if ((index - startForLengthCalculations <= width) || + (lastWordEnd == null)) { + // we should use this point, because either it doesn't actually go over the + // end (last line), or it does, but there was no earlier break point + lastWordEnd = index; + } + final String line = message.substring(start, lastWordEnd); + yield line; + addPrefix = true; + if (lastWordEnd >= message.length) return; + // just yielded a line + if (lastWordEnd == index) { + // we broke at current position + // eat all the wrappable spaces, then set our start point + // Even if some of the spaces are not wrappable that is ok. + while ((index < message.length) && (message[index] == ' ')) + index += 1; + start = index; + mode = _WordWrapParseMode.inWord; + } else { + // we broke at the previous break point, and we're at the start of a new one + assert(lastWordStart > lastWordEnd); + start = lastWordStart; + mode = _WordWrapParseMode.atBreak; + } + startForLengthCalculations = start - otherLineOffset; + assert(addPrefix); + lastWordEnd = null; + } else { + // save this break point, we're not yet over the line width + lastWordEnd = index; + // skip to the end of this break point + mode = _WordWrapParseMode.inSpace; + } + break; + } + } + } + + /// Write text ensuring the specified prefixes for the first and subsequent + /// lines. + /// + /// If [allowWrap] is true, the text may be wrapped to stay within the + /// allow `wrapWidth`. + void write(String s, {bool allowWrap = false}) { + if (s.isEmpty) return; + + final List lines = s.split('\n'); + for (int i = 0; i < lines.length; i += 1) { + if (i > 0) { + _finalizeLine(true); + _updatePrefix(); + } + final String line = lines[i]; + if (line.isNotEmpty) { + if (allowWrap && wrapWidth != null) { + final int wrapStart = _currentLine.length; + final int wrapEnd = wrapStart + line.length; + if (_wrappableRanges.isNotEmpty && + _wrappableRanges.last == wrapStart) { + // Extend last range. + _wrappableRanges.last = wrapEnd; + } else { + _wrappableRanges..add(wrapStart)..add(wrapEnd); + } + } + _currentLine.write(line); + } + } + } + + void _updatePrefix() { + if (_nextPrefixOtherLines != null) { + _prefixOtherLines = _nextPrefixOtherLines; + _nextPrefixOtherLines = null; + } + } + + void _writeLine( + String line, { + @required bool includeLineBreak, + @required bool firstLine, + }) { + line = '${_getCurrentPrefix(firstLine)}$line'; + _buffer.write(line.trimRight()); + if (includeLineBreak) _buffer.write('\n'); + _numLines++; + } + + String _getCurrentPrefix(bool firstLine) { + return _buffer.isEmpty + ? prefixLineOne + : (firstLine ? _prefixOtherLines : _prefixOtherLines); + } + + /// Write lines assuming the lines obey the specified prefixes. Ensures that + /// a newline is added if one is not present. + void writeRawLines(String lines) { + if (lines.isEmpty) return; + + if (_currentLine.isNotEmpty) { + _finalizeLine(true); + } + assert(_currentLine.isEmpty); + + _buffer.write(lines); + if (!lines.endsWith('\n')) _buffer.write('\n'); + _numLines++; + _updatePrefix(); + } + + /// Finishes the current line with a stretched version of text. + void writeStretched(String text, int targetLineLength) { + write(text); + final int currentLineLength = + _currentLine.length + _getCurrentPrefix(_buffer.isEmpty).length; + assert(_currentLine.length > 0); + final int targetLength = targetLineLength - currentLineLength; + if (targetLength > 0) { + assert(text.isNotEmpty); + final String lastChar = text[text.length - 1]; + assert(lastChar != '\n'); + _currentLine.write(lastChar * targetLength); + } + // Mark the entire line as not wrappable. + _wrappableRanges.clear(); + } + + String build() { + if (_currentLine.isNotEmpty) _finalizeLine(false); + + return _buffer.toString(); + } +} + +class _NoDefaultValue { + const _NoDefaultValue(); +} + +/// Marker object indicating that a [DiagnosticsNode] has no default value. +const _NoDefaultValue kNoDefaultValue = _NoDefaultValue(); + +bool _isSingleLine(DiagnosticsTreeStyle style) { + return style == DiagnosticsTreeStyle.singleLine; +} + +/// Renderer that creates ascii art representations of trees of +/// [DiagnosticsNode] objects. +/// +/// See also: +/// +/// * [DiagnosticsNode.toStringDeep], which uses a [TextRender] to return a +/// string representation of this node and its descendants. +class TextTreeRenderer { + /// Creates a [TextTreeRenderer] object with the given arguments specifying + /// how the tree is rendered. + /// + /// Lines are wrapped to at the maximum of [wrapWidth] and the current indent + /// plus [wrapWidthProperties] characters. This ensures that wrapping does not + /// become too excessive when displaying very deep trees and that wrapping + /// only occurs at the overall [wrapWidth] when the tree is not very indented. + /// If [maxDescendentsTruncatableNode] is specified, [DiagnosticsNode] objects + /// with `allowTruncate` set to `true` are truncated after including + /// [maxDescendentsTruncatableNode] descendants of the node to be truncated. + TextTreeRenderer({ + DiagnosticLevel minLevel = DiagnosticLevel.debug, + int wrapWidth = 100, + int wrapWidthProperties = 65, + int maxDescendentsTruncatableNode = -1, + }) : assert(minLevel != null), + _minLevel = minLevel, + _wrapWidth = wrapWidth, + _wrapWidthProperties = wrapWidthProperties, + _maxDescendentsTruncatableNode = maxDescendentsTruncatableNode; + + final int _wrapWidth; + final int _wrapWidthProperties; + final DiagnosticLevel _minLevel; + final int _maxDescendentsTruncatableNode; + + /// Text configuration to use to connect this node to a `child`. + /// + /// The singleLine styles are special cased because the connection from the + /// parent to the child should be consistent with the parent's style as the + /// single line style does not provide any meaningful style for how children + /// should be connected to their parents. + TextTreeConfiguration _childTextConfiguration( + DiagnosticsNode child, + TextTreeConfiguration textStyle, + ) { + final DiagnosticsTreeStyle childStyle = child?.style; + return (_isSingleLine(childStyle) || + childStyle == DiagnosticsTreeStyle.errorProperty) + ? textStyle + : child.textTreeConfiguration; + } + + /// Renders a [node] to a String. + String render( + DiagnosticsNode node, { + String prefixLineOne = '', + String prefixOtherLines, + TextTreeConfiguration parentConfiguration, + }) { + if (kReleaseMode) { + return ''; + } + final bool isSingleLine = _isSingleLine(node.style) && + parentConfiguration?.lineBreakProperties != true; + prefixOtherLines ??= prefixLineOne; + if (node.linePrefix != null) { + prefixLineOne += node.linePrefix; + prefixOtherLines += node.linePrefix; + } + + final TextTreeConfiguration config = node.textTreeConfiguration; + if (prefixOtherLines.isEmpty) + prefixOtherLines += config.prefixOtherLinesRootNode; + + if (node.style == DiagnosticsTreeStyle.truncateChildren) { + // This style is different enough that it isn't worthwhile to reuse the + // existing logic. + final List descendants = []; + const int maxDepth = 5; + int depth = 0; + const int maxLines = 25; + int lines = 0; + void visitor(DiagnosticsNode node) { + for (DiagnosticsNode child in node.getChildren()) { + if (lines < maxLines) { + depth += 1; + descendants.add('$prefixOtherLines${" " * depth}$child'); + if (depth < maxDepth) visitor(child); + depth -= 1; + } else if (lines == maxLines) { + descendants.add( + '$prefixOtherLines ...(descendants list truncated after $lines lines)'); + } + lines += 1; + } + } + + visitor(node); + final StringBuffer information = StringBuffer(prefixLineOne); + if (lines > 1) { + information.writeln( + 'This ${node.name} had the following descendants (showing up to depth $maxDepth):'); + } else if (descendants.length == 1) { + information.writeln('This ${node.name} had the following child:'); + } else { + information.writeln('This ${node.name} has no descendants.'); + } + information.writeAll(descendants, '\n'); + return information.toString(); + } + final _PrefixedStringBuilder builder = _PrefixedStringBuilder( + prefixLineOne: prefixLineOne, + prefixOtherLines: prefixOtherLines, + wrapWidth: + math.max(_wrapWidth, prefixOtherLines.length + _wrapWidthProperties), + ); + + List children = node.getChildren(); + + String description = + node.toDescription(parentConfiguration: parentConfiguration); + if (config.beforeName.isNotEmpty) { + builder.write(config.beforeName); + } + final bool wrapName = !isSingleLine && node.allowNameWrap; + final bool wrapDescription = !isSingleLine && node.allowWrap; + final bool uppercaseTitle = node.style == DiagnosticsTreeStyle.error; + String name = node.name; + if (uppercaseTitle) { + name = name?.toUpperCase(); + } + if (description == null || description.isEmpty) { + if (node.showName && name != null) + builder.write(name, allowWrap: wrapName); + } else { + bool includeName = false; + if (name != null && name.isNotEmpty && node.showName) { + includeName = true; + builder.write(name, allowWrap: wrapName); + if (node.showSeparator) + builder.write(config.afterName, allowWrap: wrapName); + + builder.write( + config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' ', + allowWrap: wrapName, + ); + } + if (!isSingleLine && + builder.requiresMultipleLines && + !builder.isCurrentLineEmpty) { + // Make sure there is a break between the current line and next one if + // there is not one already. + builder.write('\n'); + } + if (includeName) { + builder.incrementPrefixOtherLines( + children.isEmpty + ? config.propertyPrefixNoChildren + : config.propertyPrefixIfChildren, + updateCurrentLine: true, + ); + } + + if (uppercaseTitle) { + description = description.toUpperCase(); + } + builder.write(description.trimRight(), allowWrap: wrapDescription); + + if (!includeName) { + builder.incrementPrefixOtherLines( + children.isEmpty + ? config.propertyPrefixNoChildren + : config.propertyPrefixIfChildren, + updateCurrentLine: false, + ); + } + } + if (config.suffixLineOne.isNotEmpty) { + builder.writeStretched(config.suffixLineOne, builder.wrapWidth); + } + + final Iterable propertiesIterable = node + .getProperties() + .where((DiagnosticsNode n) => !n.isFiltered(_minLevel)); + List properties; + if (_maxDescendentsTruncatableNode >= 0 && node.allowTruncate) { + if (propertiesIterable.length < _maxDescendentsTruncatableNode) { + properties = + propertiesIterable.take(_maxDescendentsTruncatableNode).toList(); + properties.add(DiagnosticsNode.message('...')); + } else { + properties = propertiesIterable.toList(); + } + if (_maxDescendentsTruncatableNode < children.length) { + children = children.take(_maxDescendentsTruncatableNode).toList(); + children.add(DiagnosticsNode.message('...')); + } + } else { + properties = propertiesIterable.toList(); + } + + // If the node does not show a separator and there is no description then + // we should not place a separator between the name and the value. + // Essentially in this case the properties are treated a bit like a value. + if ((properties.isNotEmpty || + children.isNotEmpty || + node.emptyBodyDescription != null) && + (node.showSeparator || description?.isNotEmpty == true)) { + builder.write(config.afterDescriptionIfBody); + } + + if (config.lineBreakProperties) builder.write(config.lineBreak); + + if (properties.isNotEmpty) builder.write(config.beforeProperties); + + builder.incrementPrefixOtherLines(config.bodyIndent, + updateCurrentLine: false); + + if (node.emptyBodyDescription != null && + properties.isEmpty && + children.isEmpty && + prefixLineOne.isNotEmpty) { + builder.write(node.emptyBodyDescription); + if (config.lineBreakProperties) builder.write(config.lineBreak); + } + + for (int i = 0; i < properties.length; ++i) { + final DiagnosticsNode property = properties[i]; + if (i > 0) builder.write(config.propertySeparator); + + final TextTreeConfiguration propertyStyle = + property.textTreeConfiguration; + if (_isSingleLine(property.style)) { + // We have to treat single line properties slightly differently to deal + // with cases where a single line properties output may not have single + // linebreak. + final String propertyRender = render( + property, + prefixLineOne: '${propertyStyle.prefixLineOne}', + prefixOtherLines: + '${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}', + parentConfiguration: config, + ); + final List propertyLines = propertyRender.split('\n'); + if (propertyLines.length == 1 && !config.lineBreakProperties) { + builder.write(propertyLines.first); + } else { + builder.write(propertyRender, allowWrap: false); + if (!propertyRender.endsWith('\n')) builder.write('\n'); + } + } else { + final String propertyRender = render( + property, + prefixLineOne: + '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}', + prefixOtherLines: + '${builder.prefixOtherLines}${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}', + parentConfiguration: config, + ); + builder.writeRawLines(propertyRender); + } + } + if (properties.isNotEmpty) builder.write(config.afterProperties); + + builder.write(config.mandatoryAfterProperties); + + if (!config.lineBreakProperties) builder.write(config.lineBreak); + + final String prefixChildren = '${config.bodyIndent}'; + final String prefixChildrenRaw = '$prefixOtherLines$prefixChildren'; + if (children.isEmpty && + config.addBlankLineIfNoChildren && + builder.requiresMultipleLines && + builder.prefixOtherLines.trimRight().isNotEmpty) { + builder.write(config.lineBreak); + } + + if (children.isNotEmpty && config.showChildren) { + if (config.isBlankLineBetweenPropertiesAndChildren && + properties.isNotEmpty && + children.first.textTreeConfiguration + .isBlankLineBetweenPropertiesAndChildren) { + builder.write(config.lineBreak); + } + + builder.prefixOtherLines = prefixOtherLines; + + for (int i = 0; i < children.length; i++) { + final DiagnosticsNode child = children[i]; + assert(child != null); + final TextTreeConfiguration childConfig = + _childTextConfiguration(child, config); + if (i == children.length - 1) { + final String lastChildPrefixLineOne = + '$prefixChildrenRaw${childConfig.prefixLastChildLineOne}'; + final String childPrefixOtherLines = + '$prefixChildrenRaw${childConfig.childLinkSpace}${childConfig.prefixOtherLines}'; + builder.writeRawLines(render( + child, + prefixLineOne: lastChildPrefixLineOne, + prefixOtherLines: childPrefixOtherLines, + parentConfiguration: config, + )); + if (childConfig.footer.isNotEmpty) { + builder.prefixOtherLines = prefixChildrenRaw; + builder.write('${childConfig.childLinkSpace}${childConfig.footer}'); + if (childConfig.manditoryFooter.isNotEmpty) { + builder.writeStretched( + childConfig.manditoryFooter, + math.max(builder.wrapWidth, + _wrapWidthProperties + childPrefixOtherLines.length), + ); + } + builder.write(config.lineBreak); + } + } else { + final TextTreeConfiguration nextChildStyle = + _childTextConfiguration(children[i + 1], config); + final String childPrefixLineOne = + '$prefixChildrenRaw${childConfig.prefixLineOne}'; + final String childPrefixOtherLines = + '$prefixChildrenRaw${nextChildStyle.linkCharacter}${childConfig.prefixOtherLines}'; + builder.writeRawLines(render( + child, + prefixLineOne: childPrefixLineOne, + prefixOtherLines: childPrefixOtherLines, + parentConfiguration: config, + )); + if (childConfig.footer.isNotEmpty) { + builder.prefixOtherLines = prefixChildrenRaw; + builder.write('${childConfig.linkCharacter}${childConfig.footer}'); + if (childConfig.manditoryFooter.isNotEmpty) { + builder.writeStretched( + childConfig.manditoryFooter, + math.max(builder.wrapWidth, + _wrapWidthProperties + childPrefixOtherLines.length), + ); + } + builder.write(config.lineBreak); + } + } + } + } + if (parentConfiguration == null && config.manditoryFooter.isNotEmpty) { + builder.writeStretched(config.manditoryFooter, builder.wrapWidth); + builder.write(config.lineBreak); + } + return builder.build(); + } +} + +/// Defines diagnostics data for a [value]. +/// +/// For debug and profile modes, [DiagnosticsNode] provides a high quality +/// multi-line string dump via [toStringDeep]. The core members are the [name], +/// [toDescription], [getProperties], [value], and [getChildren]. All other +/// members exist typically to provide hints for how [toStringDeep] and +/// debugging tools should format output. +/// +/// In release mode, far less information is retained and some information may +/// not print at all. +abstract class DiagnosticsNode { + /// Initializes the object. + /// + /// The [style], [showName], and [showSeparator] arguments must not + /// be null. + DiagnosticsNode({ + @required this.name, + this.style, + this.showName = true, + this.showSeparator = true, + this.linePrefix, + }) : assert(showName != null), + assert(showSeparator != null), + // A name ending with ':' indicates that the user forgot that the ':' will + // be automatically added for them when generating descriptions of the + // property. + assert( + name == null || !name.endsWith(':'), + 'Names of diagnostic nodes must not end with colons.\n' + 'name:\n' + ' "$name"'); + + /// Diagnostics containing just a string `message` and not a concrete name or + /// value. + /// + /// The [style] and [level] arguments must not be null. + /// + /// See also: + /// + /// * [MessageProperty], which is better suited to messages that are to be + /// formatted like a property with a separate name and message. + factory DiagnosticsNode.message( + String message, { + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + bool allowWrap = true, + }) { + assert(style != null); + assert(level != null); + return DiagnosticsProperty( + '', + null, + description: message, + style: style, + showName: false, + allowWrap: allowWrap, + level: level, + ); + } + + /// Label describing the [DiagnosticsNode], typically shown before a separator + /// (see [showSeparator]). + /// + /// The name will be omitted if the [showName] property is false. + final String name; + + /// Returns a description with a short summary of the node itself not + /// including children or properties. + /// + /// `parentConfiguration` specifies how the parent is rendered as text art. + /// For example, if the parent does not line break between properties, the + /// description of a property should also be a single line if possible. + String toDescription({TextTreeConfiguration parentConfiguration}); + + /// Whether to show a separator between [name] and description. + /// + /// If false, name and description should be shown with no separation. + /// `:` is typically used as a separator when displaying as text. + final bool showSeparator; + + /// Whether the diagnostic should be filtered due to its [level] being lower + /// than `minLevel`. + /// + /// If `minLevel` is [DiagnosticLevel.hidden] no diagnostics will be filtered. + /// If `minLevel` is [DiagnosticLevel.off] all diagnostics will be filtered. + bool isFiltered(DiagnosticLevel minLevel) => + kReleaseMode || level.index < minLevel.index; + + /// Priority level of the diagnostic used to control which diagnostics should + /// be shown and filtered. + /// + /// Typically this only makes sense to set to a different value than + /// [DiagnosticLevel.info] for diagnostics representing properties. Some + /// subclasses have a `level` argument to their constructor which influences + /// the value returned here but other factors also influence it. For example, + /// whether an exception is thrown computing a property value + /// [DiagnosticLevel.error] is returned. + DiagnosticLevel get level => + kReleaseMode ? DiagnosticLevel.hidden : DiagnosticLevel.info; + + /// Whether the name of the property should be shown when showing the default + /// view of the tree. + /// + /// This could be set to false (hiding the name) if the value's description + /// will make the name self-evident. + final bool showName; + + /// Prefix to include at the start of each line + final String linePrefix; + + /// Description to show if the node has no displayed properties or children. + String get emptyBodyDescription => null; + + /// The actual object this is diagnostics data for. + Object get value; + + /// Hint for how the node should be displayed. + final DiagnosticsTreeStyle style; + + /// Whether to wrap text on onto multiple lines or not. + bool get allowWrap => false; + + /// Whether to wrap the name onto multiple lines or not. + bool get allowNameWrap => false; + + /// Whether to allow truncation when displaying the node and its children. + bool get allowTruncate => false; + + /// Properties of this [DiagnosticsNode]. + /// + /// Properties and children are kept distinct even though they are both + /// [List] because they should be grouped differently. + List getProperties(); + + /// Children of this [DiagnosticsNode]. + /// + /// See also: + /// + /// * [getProperties] + List getChildren(); + + String get _separator => showSeparator ? ':' : ''; + + /// Serialize the node to a JSON map according to the configuration provided + /// in the [DiagnosticsSerializationDelegate]. + /// + /// Subclasses should override if they have additional properties that are + /// useful for the GUI tools that consume this JSON. + /// + /// See also: + /// + /// * [WidgetInspectorService], which forms the bridge between JSON returned + /// by this method and interactive tree views in the Flutter IntelliJ + /// plugin. + @mustCallSuper + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + if (kReleaseMode) { + return {}; + } + final bool hasChildren = getChildren().isNotEmpty; + return { + 'description': toDescription(), + 'type': runtimeType.toString(), + if (name != null) 'name': name, + if (!showSeparator) 'showSeparator': showSeparator, + if (level != DiagnosticLevel.info) 'level': describeEnum(level), + if (showName == false) 'showName': showName, + if (emptyBodyDescription != null) + 'emptyBodyDescription': emptyBodyDescription, + if (style != DiagnosticsTreeStyle.sparse) 'style': describeEnum(style), + if (allowTruncate) 'allowTruncate': allowTruncate, + if (hasChildren) 'hasChildren': hasChildren, + if (linePrefix?.isNotEmpty == true) 'linePrefix': linePrefix, + if (!allowWrap) 'allowWrap': allowWrap, + if (allowNameWrap) 'allowNameWrap': allowNameWrap, + ...delegate.additionalNodeProperties(this), + if (delegate.includeProperties) + 'properties': toJsonList( + delegate.filterProperties(getProperties(), this), + this, + delegate, + ), + if (delegate.subtreeDepth > 0) + 'children': toJsonList( + delegate.filterChildren(getChildren(), this), + this, + delegate, + ), + }; + } + + /// Serializes a [List] of [DiagnosticsNode]s to a JSON list according to + /// the configuration provided by the [DiagnosticsSerializationDelegate]. + /// + /// The provided `nodes` may be properties or children of the `parent` + /// [DiagnosticsNode]. + static List> toJsonList( + List nodes, + DiagnosticsNode parent, + DiagnosticsSerializationDelegate delegate, + ) { + bool truncated = false; + if (nodes == null) return const >[]; + final int originalNodeCount = nodes.length; + nodes = delegate.truncateNodesList(nodes, parent); + if (nodes.length != originalNodeCount) { + nodes.add(DiagnosticsNode.message('...')); + truncated = true; + } + final List> json = + nodes.map>((DiagnosticsNode node) { + return node.toJsonMap(delegate.delegateForNode(node)); + }).toList(); + if (truncated) json.last['truncated'] = true; + return json; + } + + /// Returns a string representation of this diagnostic that is compatible with + /// the style of the parent if the node is not the root. + /// + /// `parentConfiguration` specifies how the parent is rendered as text art. + /// For example, if the parent places all properties on one line, the + /// [toString] for each property should avoid line breaks if possible. + /// + /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included + /// in the output. + @override + String toString({ + TextTreeConfiguration parentConfiguration, + DiagnosticLevel minLevel = DiagnosticLevel.info, + }) { + if (kReleaseMode) { + return super.toString(); + } + assert(style != null); + assert(minLevel != null); + if (_isSingleLine(style)) + return toStringDeep( + parentConfiguration: parentConfiguration, minLevel: minLevel); + + final String description = + toDescription(parentConfiguration: parentConfiguration); + + if (name == null || name.isEmpty || !showName) return description; + + return description.contains('\n') + ? '$name$_separator\n$description' + : '$name$_separator $description'; + } + + /// Returns a configuration specifying how this object should be rendered + /// as text art. + @protected + TextTreeConfiguration get textTreeConfiguration { + assert(style != null); + switch (style) { + case DiagnosticsTreeStyle.none: + return null; + case DiagnosticsTreeStyle.dense: + return denseTextConfiguration; + case DiagnosticsTreeStyle.sparse: + return sparseTextConfiguration; + case DiagnosticsTreeStyle.offstage: + return dashedTextConfiguration; + case DiagnosticsTreeStyle.whitespace: + return whitespaceTextConfiguration; + case DiagnosticsTreeStyle.transition: + return transitionTextConfiguration; + case DiagnosticsTreeStyle.singleLine: + return singleLineTextConfiguration; + case DiagnosticsTreeStyle.errorProperty: + return errorPropertyTextConfiguration; + case DiagnosticsTreeStyle.shallow: + return shallowTextConfiguration; + case DiagnosticsTreeStyle.error: + return errorTextConfiguration; + case DiagnosticsTreeStyle.truncateChildren: + // Truncate children doesn't really need its own text style as the + // rendering is quite custom. + return whitespaceTextConfiguration; + case DiagnosticsTreeStyle.flat: + return flatTextConfiguration; + } + return null; + } + + /// Returns a string representation of this node and its descendants. + /// + /// `prefixLineOne` will be added to the front of the first line of the + /// output. `prefixOtherLines` will be added to the front of each other line. + /// If `prefixOtherLines` is null, the `prefixLineOne` is used for every line. + /// By default, there is no prefix. + /// + /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included + /// in the output. + /// + /// The [toStringDeep] method takes other arguments, but those are intended + /// for internal use when recursing to the descendants, and so can be ignored. + /// + /// See also: + /// + /// * [toString], for a brief description of the [value] but not its + /// children. + String toStringDeep({ + String prefixLineOne = '', + String prefixOtherLines, + TextTreeConfiguration parentConfiguration, + DiagnosticLevel minLevel = DiagnosticLevel.debug, + }) { + if (kReleaseMode) { + return ''; + } + return TextTreeRenderer( + minLevel: minLevel, + wrapWidth: 65, + wrapWidthProperties: 65, + ).render( + this, + prefixLineOne: prefixLineOne, + prefixOtherLines: prefixOtherLines, + parentConfiguration: parentConfiguration, + ); + } +} + +/// Debugging message displayed like a property. +/// +/// {@tool sample} +/// +/// The following two properties are better expressed using this +/// [MessageProperty] class, rather than [StringProperty], as the intent is to +/// show a message with property style display rather than to describe the value +/// of an actual property of the object: +/// +/// ```dart +/// var table = MessageProperty('table size', '$columns\u00D7$rows'); +/// var usefulness = MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)'); +/// ``` +/// {@end-tool} +/// {@tool sample} +/// +/// On the other hand, [StringProperty] is better suited when the property has a +/// concrete value that is a string: +/// +/// ```dart +/// var name = StringProperty('name', _name); +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [DiagnosticsNode.message], which serves the same role for messages +/// without a clear property name. +/// * [StringProperty], which is a better fit for properties with string values. +class MessageProperty extends DiagnosticsProperty { + /// Create a diagnostics property that displays a message. + /// + /// Messages have no concrete [value] (so [value] will return null). The + /// message is stored as the description. + /// + /// The [name], `message`, and [level] arguments must not be null. + MessageProperty( + String name, + String message, { + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(name != null), + assert(message != null), + assert(style != null), + assert(level != null), + super(name, null, description: message, style: style, level: level); +} + +/// Property which encloses its string [value] in quotes. +/// +/// See also: +/// +/// * [MessageProperty], which is a better fit for showing a message +/// instead of describing a property with a string value. +class StringProperty extends DiagnosticsProperty { + /// Create a diagnostics property for strings. + /// + /// The [showName], [quoted], [style], and [level] arguments must not be null. + StringProperty( + String name, + String value, { + String description, + String tooltip, + bool showName = true, + Object defaultValue = kNoDefaultValue, + this.quoted = true, + String ifEmpty, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(quoted != null), + assert(style != null), + assert(level != null), + super( + name, + value, + description: description, + defaultValue: defaultValue, + tooltip: tooltip, + showName: showName, + ifEmpty: ifEmpty, + style: style, + level: level, + ); + + /// Whether the value is enclosed in double quotes. + final bool quoted; + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + json['quoted'] = quoted; + return json; + } + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + String text = _description ?? value; + if (parentConfiguration != null && + !parentConfiguration.lineBreakProperties && + text != null) { + // Escape linebreaks in multiline strings to avoid confusing output when + // the parent of this node is trying to display all properties on the same + // line. + text = text.replaceAll('\n', '\\n'); + } + + if (quoted && text != null) { + // An empty value would not appear empty after being surrounded with + // quotes so we have to handle this case separately. + if (ifEmpty != null && text.isEmpty) return ifEmpty; + return '"$text"'; + } + return text.toString(); + } +} + +abstract class _NumProperty extends DiagnosticsProperty { + _NumProperty( + String name, + T value, { + String ifNull, + this.unit, + bool showName = true, + Object defaultValue = kNoDefaultValue, + String tooltip, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : super( + name, + value, + ifNull: ifNull, + showName: showName, + defaultValue: defaultValue, + tooltip: tooltip, + level: level, + style: style, + ); + + _NumProperty.lazy( + String name, + ComputePropertyValueCallback computeValue, { + String ifNull, + this.unit, + bool showName = true, + Object defaultValue = kNoDefaultValue, + String tooltip, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : super.lazy( + name, + computeValue, + ifNull: ifNull, + showName: showName, + defaultValue: defaultValue, + tooltip: tooltip, + style: style, + level: level, + ); + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (unit != null) json['unit'] = unit; + + json['numberToString'] = numberToString(); + return json; + } + + /// Optional unit the [value] is measured in. + /// + /// Unit must be acceptable to display immediately after a number with no + /// spaces. For example: 'physical pixels per logical pixel' should be a + /// [tooltip] not a [unit]. + final String unit; + + /// String describing just the numeric [value] without a unit suffix. + String numberToString(); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value == null) return value.toString(); + + return unit != null ? '${numberToString()}$unit' : numberToString(); + } +} + +/// Property describing a [double] [value] with an optional [unit] of measurement. +/// +/// Numeric formatting is optimized for debug message readability. +class DoubleProperty extends _NumProperty { + /// If specified, [unit] describes the unit for the [value] (e.g. px). + /// + /// The [showName], [style], and [level] arguments must not be null. + DoubleProperty( + String name, + double value, { + String ifNull, + String unit, + String tooltip, + Object defaultValue = kNoDefaultValue, + bool showName = true, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(style != null), + assert(level != null), + super( + name, + value, + ifNull: ifNull, + unit: unit, + tooltip: tooltip, + defaultValue: defaultValue, + showName: showName, + style: style, + level: level, + ); + + /// Property with a [value] that is computed only when needed. + /// + /// Use if computing the property [value] may throw an exception or is + /// expensive. + /// + /// The [showName] and [level] arguments must not be null. + DoubleProperty.lazy( + String name, + ComputePropertyValueCallback computeValue, { + String ifNull, + bool showName = true, + String unit, + String tooltip, + Object defaultValue = kNoDefaultValue, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(level != null), + super.lazy( + name, + computeValue, + showName: showName, + ifNull: ifNull, + unit: unit, + tooltip: tooltip, + defaultValue: defaultValue, + level: level, + ); + + @override + String numberToString() => debugFormatDouble(value); +} + +/// An int valued property with an optional unit the value is measured in. +/// +/// Examples of units include 'px' and 'ms'. +class IntProperty extends _NumProperty { + /// Create a diagnostics property for integers. + /// + /// The [showName], [style], and [level] arguments must not be null. + IntProperty( + String name, + int value, { + String ifNull, + bool showName = true, + String unit, + Object defaultValue = kNoDefaultValue, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(level != null), + assert(style != null), + super( + name, + value, + ifNull: ifNull, + showName: showName, + unit: unit, + defaultValue: defaultValue, + level: level, + ); + + @override + String numberToString() => value.toString(); +} + +/// Property which clamps a [double] to between 0 and 1 and formats it as a +/// percentage. +class PercentProperty extends DoubleProperty { + /// Create a diagnostics property for doubles that represent percentages or + /// fractions. + /// + /// Setting [showName] to false is often reasonable for [PercentProperty] + /// objects, as the fact that the property is shown as a percentage tends to + /// be sufficient to disambiguate its meaning. + /// + /// The [showName] and [level] arguments must not be null. + PercentProperty( + String name, + double fraction, { + String ifNull, + bool showName = true, + String tooltip, + String unit, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(level != null), + super( + name, + fraction, + ifNull: ifNull, + showName: showName, + tooltip: tooltip, + unit: unit, + level: level, + ); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value == null) return value.toString(); + return unit != null ? '${numberToString()} $unit' : numberToString(); + } + + @override + String numberToString() { + if (value == null) return value.toString(); + return '${(value.clamp(0.0, 1.0) * 100.0).toStringAsFixed(1)}%'; + } +} + +/// Property where the description is either [ifTrue] or [ifFalse] depending on +/// whether [value] is true or false. +/// +/// Using [FlagProperty] instead of [DiagnosticsProperty] can make +/// diagnostics display more polished. For example, given a property named +/// `visible` that is typically true, the following code will return 'hidden' +/// when `visible` is false and nothing when visible is true, in contrast to +/// `visible: true` or `visible: false`. +/// +/// {@tool sample} +/// +/// ```dart +/// FlagProperty( +/// 'visible', +/// value: true, +/// ifFalse: 'hidden', +/// ) +/// ``` +/// {@end-tool} +/// {@tool sample} +/// +/// [FlagProperty] should also be used instead of [DiagnosticsProperty] +/// if showing the bool value would not clearly indicate the meaning of the +/// property value. +/// +/// ```dart +/// FlagProperty( +/// 'inherit', +/// value: inherit, +/// ifTrue: '', +/// ifFalse: '', +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [ObjectFlagProperty], which provides similar behavior describing whether +/// a [value] is null. +class FlagProperty extends DiagnosticsProperty { + /// Constructs a FlagProperty with the given descriptions with the specified descriptions. + /// + /// [showName] defaults to false as typically [ifTrue] and [ifFalse] should + /// be descriptions that make the property name redundant. + /// + /// The [showName] and [level] arguments must not be null. + FlagProperty( + String name, { + @required bool value, + this.ifTrue, + this.ifFalse, + bool showName = false, + Object defaultValue, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(level != null), + assert(ifTrue != null || ifFalse != null), + super( + name, + value, + showName: showName, + defaultValue: defaultValue, + level: level, + ); + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (ifTrue != null) json['ifTrue'] = ifTrue; + if (ifFalse != null) json['ifFalse'] = ifFalse; + + return json; + } + + /// Description to use if the property [value] is true. + /// + /// If not specified and [value] equals true the property's priority [level] + /// will be [DiagnosticLevel.hidden]. + final String ifTrue; + + /// Description to use if the property value is false. + /// + /// If not specified and [value] equals false, the property's priority [level] + /// will be [DiagnosticLevel.hidden]. + final String ifFalse; + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value == true) { + if (ifTrue != null) return ifTrue; + } else if (value == false) { + if (ifFalse != null) return ifFalse; + } + return super.valueToString(parentConfiguration: parentConfiguration); + } + + @override + bool get showName { + if (value == null || + (value == true && ifTrue == null) || + (value == false && ifFalse == null)) { + // We are missing a description for the flag value so we need to show the + // flag name. The property will have DiagnosticLevel.hidden for this case + // so users will not see this the property in this case unless they are + // displaying hidden properties. + return true; + } + return super.showName; + } + + @override + DiagnosticLevel get level { + if (value == true) { + if (ifTrue == null) return DiagnosticLevel.hidden; + } + if (value == false) { + if (ifFalse == null) return DiagnosticLevel.hidden; + } + return super.level; + } +} + +/// Property with an `Iterable` [value] that can be displayed with +/// different [DiagnosticsTreeStyle] for custom rendering. +/// +/// If [style] is [DiagnosticsTreeStyle.singleLine], the iterable is described +/// as a comma separated list, otherwise the iterable is described as a line +/// break separated list. +class IterableProperty extends DiagnosticsProperty> { + /// Create a diagnostics property for iterables (e.g. lists). + /// + /// The [ifEmpty] argument is used to indicate how an iterable [value] with 0 + /// elements is displayed. If [ifEmpty] equals null that indicates that an + /// empty iterable [value] is not interesting to display similar to how + /// [defaultValue] is used to indicate that a specific concrete value is not + /// interesting to display. + /// + /// The [style], [showName], [showSeparator], and [level] arguments must not be null. + IterableProperty( + String name, + Iterable value, { + Object defaultValue = kNoDefaultValue, + String ifNull, + String ifEmpty = '[]', + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + bool showName = true, + bool showSeparator = true, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(style != null), + assert(showName != null), + assert(showSeparator != null), + assert(level != null), + super( + name, + value, + defaultValue: defaultValue, + ifNull: ifNull, + ifEmpty: ifEmpty, + style: style, + showName: showName, + showSeparator: showSeparator, + level: level, + ); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value == null) return value.toString(); + + if (value.isEmpty) return ifEmpty ?? '[]'; + + final Iterable formattedValues = value.map((T v) { + if (T == double && v is double) { + return debugFormatDouble(v); + } else { + return v.toString(); + } + }); + + if (parentConfiguration != null && + !parentConfiguration.lineBreakProperties) { + // Always display the value as a single line and enclose the iterable + // value in brackets to avoid ambiguity. + return '[${formattedValues.join(', ')}]'; + } + + return formattedValues.join(_isSingleLine(style) ? ', ' : '\n'); + } + + /// Priority level of the diagnostic used to control which diagnostics should + /// be shown and filtered. + /// + /// If [ifEmpty] is null and the [value] is an empty [Iterable] then level + /// [DiagnosticLevel.fine] is returned in a similar way to how an + /// [ObjectFlagProperty] handles when [ifNull] is null and the [value] is + /// null. + @override + DiagnosticLevel get level { + if (ifEmpty == null && + value != null && + value.isEmpty && + super.level != DiagnosticLevel.hidden) return DiagnosticLevel.fine; + return super.level; + } + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (value != null) { + json['values'] = + value.map((T value) => value.toString()).toList(); + } + return json; + } +} + +/// An property than displays enum values tersely. +/// +/// The enum value is displayed with the class name stripped. For example: +/// [HitTestBehavior.deferToChild] is shown as `deferToChild`. +/// +/// See also: +/// +/// * [DiagnosticsProperty] which documents named parameters common to all +/// [DiagnosticsProperty]. +class EnumProperty extends DiagnosticsProperty { + /// Create a diagnostics property that displays an enum. + /// + /// The [level] argument must also not be null. + EnumProperty( + String name, + T value, { + Object defaultValue = kNoDefaultValue, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(level != null), + super( + name, + value, + defaultValue: defaultValue, + level: level, + ); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value == null) return value.toString(); + return describeEnum(value); + } +} + +/// A property where the important diagnostic information is primarily whether +/// the [value] is present (non-null) or absent (null), rather than the actual +/// value of the property itself. +/// +/// The [ifPresent] and [ifNull] strings describe the property [value] when it +/// is non-null and null respectively. If one of [ifPresent] or [ifNull] is +/// omitted, that is taken to mean that [level] should be +/// [DiagnosticLevel.hidden] when [value] is non-null or null respectively. +/// +/// This kind of diagnostics property is typically used for opaque +/// values, like closures, where presenting the actual object is of dubious +/// value but where reporting the presence or absence of the value is much more +/// useful. +/// +/// See also: +/// +/// +/// * [FlagsSummary], which provides similar functionality but accepts multiple +/// flags under the same name, and is preferred if there are multiple such +/// values that can fit into a same category (such as "listeners"). +/// * [FlagProperty], which provides similar functionality describing whether +/// a [value] is true or false. +class ObjectFlagProperty extends DiagnosticsProperty { + /// Create a diagnostics property for values that can be present (non-null) or + /// absent (null), but for which the exact value's [Object.toString] + /// representation is not very transparent (e.g. a callback). + /// + /// The [showName] and [level] arguments must not be null. Additionally, at + /// least one of [ifPresent] and [ifNull] must not be null. + ObjectFlagProperty( + String name, + T value, { + this.ifPresent, + String ifNull, + bool showName = false, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(ifPresent != null || ifNull != null), + assert(showName != null), + assert(level != null), + super( + name, + value, + showName: showName, + ifNull: ifNull, + level: level, + ); + + /// Shorthand constructor to describe whether the property has a value. + /// + /// Only use if prefixing the property name with the word 'has' is a good + /// flag name. + /// + /// The [name] and [level] arguments must not be null. + ObjectFlagProperty.has( + String name, + T value, { + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(name != null), + assert(level != null), + ifPresent = 'has $name', + super( + name, + value, + showName: false, + level: level, + ); + + /// Description to use if the property [value] is not null. + /// + /// If the property [value] is not null and [ifPresent] is null, the + /// [level] for the property is [DiagnosticLevel.hidden] and the description + /// from superclass is used. + final String ifPresent; + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + if (value != null) { + if (ifPresent != null) return ifPresent; + } else { + if (ifNull != null) return ifNull; + } + return super.valueToString(parentConfiguration: parentConfiguration); + } + + @override + bool get showName { + if ((value != null && ifPresent == null) || + (value == null && ifNull == null)) { + // We are missing a description for the flag value so we need to show the + // flag name. The property will have DiagnosticLevel.hidden for this case + // so users will not see this the property in this case unless they are + // displaying hidden properties. + return true; + } + return super.showName; + } + + @override + DiagnosticLevel get level { + if (value != null) { + if (ifPresent == null) return DiagnosticLevel.hidden; + } else { + if (ifNull == null) return DiagnosticLevel.hidden; + } + + return super.level; + } + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (ifPresent != null) json['ifPresent'] = ifPresent; + return json; + } +} + +/// A summary of multiple properties, indicating whether each of them is present +/// (non-null) or absent (null). +/// +/// Each entry of [value] is described by its key. The eventual description will +/// be a list of keys of non-null entries. +/// +/// The [ifEmpty] describes the entire collection of [value] when it contains no +/// non-null entries. If [ifEmpty] is omitted, [level] will be +/// [DiagnosticLevel.hidden] when [value] contains no non-null entries. +/// +/// This kind of diagnostics property is typically used for opaque +/// values, like closures, where presenting the actual object is of dubious +/// value but where reporting the presence or absence of the value is much more +/// useful. +/// +/// See also: +/// +/// * [ObjectFlagProperty], which provides similar functionality but accepts +/// only one flag, and is preferred if there is only one entry. +/// * [IterableProperty], which provides similar functionality describing +/// the values a collection of objects. +class FlagsSummary extends DiagnosticsProperty> { + /// Create a summary for multiple properties, indicating whether each of them + /// is present (non-null) or absent (null). + /// + /// The [value], [showName], [showSeparator] and [level] arguments must not be + /// null. + FlagsSummary( + String name, + Map value, { + String ifEmpty, + bool showName = true, + bool showSeparator = true, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(value != null), + assert(showName != null), + assert(showSeparator != null), + assert(level != null), + super( + name, + value, + ifEmpty: ifEmpty, + showName: showName, + showSeparator: showSeparator, + level: level, + ); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + assert(value != null); + if (!_hasNonNullEntry() && ifEmpty != null) return ifEmpty; + + final Iterable formattedValues = _formattedValues(); + if (parentConfiguration != null && + !parentConfiguration.lineBreakProperties) { + // Always display the value as a single line and enclose the iterable + // value in brackets to avoid ambiguity. + return '[${formattedValues.join(', ')}]'; + } + + return formattedValues.join(_isSingleLine(style) ? ', ' : '\n'); + } + + /// Priority level of the diagnostic used to control which diagnostics should + /// be shown and filtered. + /// + /// If [ifEmpty] is null and the [value] contains no non-null entries, then + /// level [DiagnosticLevel.hidden] is returned. + @override + DiagnosticLevel get level { + if (!_hasNonNullEntry() && ifEmpty == null) return DiagnosticLevel.hidden; + return super.level; + } + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (value.isNotEmpty) json['values'] = _formattedValues().toList(); + return json; + } + + bool _hasNonNullEntry() => value.values.any((Object o) => o != null); + + // An iterable of each entry's description in [value]. + // + // For a non-null value, its description is its key. + // + // For a null value, it is omitted unless `includeEmtpy` is true and + // [ifEntryNull] contains a corresponding description. + Iterable _formattedValues() sync* { + for (MapEntry entry in value.entries) { + if (entry.value != null) { + yield entry.key; + } + } + } +} + +/// Signature for computing the value of a property. +/// +/// May throw exception if accessing the property would throw an exception +/// and callers must handle that case gracefully. For example, accessing a +/// property may trigger an assert that layout constraints were violated. +typedef ComputePropertyValueCallback = T Function(); + +/// Property with a [value] of type [T]. +/// +/// If the default `value.toString()` does not provide an adequate description +/// of the value, specify `description` defining a custom description. +/// +/// The [showSeparator] property indicates whether a separator should be placed +/// between the property [name] and its [value]. +class DiagnosticsProperty extends DiagnosticsNode { + /// Create a diagnostics property. + /// + /// The [showName], [showSeparator], [style], [missingIfNull], and [level] + /// arguments must not be null. + /// + /// The [level] argument is just a suggestion and can be overridden if + /// something else about the property causes it to have a lower or higher + /// level. For example, if the property value is null and [missingIfNull] is + /// true, [level] is raised to [DiagnosticLevel.warning]. + DiagnosticsProperty( + String name, + T value, { + String description, + String ifNull, + this.ifEmpty, + bool showName = true, + bool showSeparator = true, + this.defaultValue = kNoDefaultValue, + this.tooltip, + this.missingIfNull = false, + String linePrefix, + this.expandableValue = false, + this.allowWrap = true, + this.allowNameWrap = true, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(showSeparator != null), + assert(style != null), + assert(level != null), + _description = description, + _valueComputed = true, + _value = value, + _computeValue = null, + ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null), + _defaultLevel = level, + super( + name: name, + showName: showName, + showSeparator: showSeparator, + style: style, + linePrefix: linePrefix, + ); + + /// Property with a [value] that is computed only when needed. + /// + /// Use if computing the property [value] may throw an exception or is + /// expensive. + /// + /// The [showName], [showSeparator], [style], [missingIfNull], and [level] + /// arguments must not be null. + /// + /// The [level] argument is just a suggestion and can be overridden if + /// if something else about the property causes it to have a lower or higher + /// level. For example, if calling `computeValue` throws an exception, [level] + /// will always return [DiagnosticLevel.error]. + DiagnosticsProperty.lazy( + String name, + ComputePropertyValueCallback computeValue, { + String description, + String ifNull, + this.ifEmpty, + bool showName = true, + bool showSeparator = true, + this.defaultValue = kNoDefaultValue, + this.tooltip, + this.missingIfNull = false, + this.expandableValue = false, + this.allowWrap = true, + this.allowNameWrap = true, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(showName != null), + assert(showSeparator != null), + assert(defaultValue == kNoDefaultValue || defaultValue is T), + assert(missingIfNull != null), + assert(style != null), + assert(level != null), + _description = description, + _valueComputed = false, + _value = null, + _computeValue = computeValue, + _defaultLevel = level, + ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null), + super( + name: name, + showName: showName, + showSeparator: showSeparator, + style: style, + ); + + final String _description; + + /// Whether to expose properties and children of the value as properties and + /// children. + final bool expandableValue; + + @override + final bool allowWrap; + + @override + final bool allowNameWrap; + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final T v = value; + List> properties; + if (delegate.expandPropertyValues && + delegate.includeProperties && + v is DiagnosticableMixin && + getProperties().isEmpty) { + // Exclude children for expanded nodes to avoid cycles. + delegate = delegate.copyWith(subtreeDepth: 0, includeProperties: false); + properties = DiagnosticsNode.toJsonList( + delegate.filterProperties(v.toDiagnosticsNode().getProperties(), this), + this, + delegate, + ); + } + final Map json = super.toJsonMap(delegate); + if (properties != null) { + json['properties'] = properties; + } + if (defaultValue != kNoDefaultValue) + json['defaultValue'] = defaultValue.toString(); + if (ifEmpty != null) json['ifEmpty'] = ifEmpty; + if (ifNull != null) json['ifNull'] = ifNull; + if (tooltip != null) json['tooltip'] = tooltip; + json['missingIfNull'] = missingIfNull; + if (exception != null) json['exception'] = exception.toString(); + json['propertyType'] = propertyType.toString(); + json['defaultLevel'] = describeEnum(_defaultLevel); + if (value is DiagnosticableMixin || value is DiagnosticsNode) + json['isDiagnosticableValue'] = true; + if (v is num) + // Workaround for https://github.com/flutter/flutter/issues/39937#issuecomment-529558033. + // JSON.stringify replaces infinity and NaN with null. + json['value'] = v.isFinite ? v : v.toString(); + if (value is String || value is bool || value == null) + json['value'] = value; + return json; + } + + /// Returns a string representation of the property value. + /// + /// Subclasses should override this method instead of [toDescription] to + /// customize how property values are converted to strings. + /// + /// Overriding this method ensures that behavior controlling how property + /// values are decorated to generate a nice [toDescription] are consistent + /// across all implementations. Debugging tools may also choose to use + /// [valueToString] directly instead of [toDescription]. + /// + /// `parentConfiguration` specifies how the parent is rendered as text art. + /// For example, if the parent places all properties on one line, the value + /// of the property should be displayed without line breaks if possible. + String valueToString({TextTreeConfiguration parentConfiguration}) { + final T v = value; + // DiagnosticableTree values are shown using the shorter toStringShort() + // instead of the longer toString() because the toString() for a + // DiagnosticableTree value is likely too large to be useful. + return (v is DiagnosticableTree ? v.toStringShort() : v.toString()) ?? ''; + } + + @override + String toDescription({TextTreeConfiguration parentConfiguration}) { + if (_description != null) return _addTooltip(_description); + + if (exception != null) return 'EXCEPTION (${exception.runtimeType})'; + + if (ifNull != null && value == null) return _addTooltip(ifNull); + + String result = valueToString(parentConfiguration: parentConfiguration); + if (result.isEmpty && ifEmpty != null) result = ifEmpty; + return _addTooltip(result); + } + + /// If a [tooltip] is specified, add the tooltip it to the end of `text` + /// enclosing it parenthesis to disambiguate the tooltip from the rest of + /// the text. + /// + /// `text` must not be null. + String _addTooltip(String text) { + assert(text != null); + return tooltip == null ? text : '$text ($tooltip)'; + } + + /// Description if the property [value] is null. + final String ifNull; + + /// Description if the property description would otherwise be empty. + final String ifEmpty; + + /// Optional tooltip typically describing the property. + /// + /// Example tooltip: 'physical pixels per logical pixel' + /// + /// If present, the tooltip is added in parenthesis after the raw value when + /// generating the string description. + final String tooltip; + + /// Whether a [value] of null causes the property to have [level] + /// [DiagnosticLevel.warning] warning that the property is missing a [value]. + final bool missingIfNull; + + /// The type of the property [value]. + /// + /// This is determined from the type argument `T` used to instantiate the + /// [DiagnosticsProperty] class. This means that the type is available even if + /// [value] is null, but it also means that the [propertyType] is only as + /// accurate as the type provided when invoking the constructor. + /// + /// Generally, this is only useful for diagnostic tools that should display + /// null values in a manner consistent with the property type. For example, a + /// tool might display a null [Color] value as an empty rectangle instead of + /// the word "null". + Type get propertyType => T; + + /// Returns the value of the property either from cache or by invoking a + /// [ComputePropertyValueCallback]. + /// + /// If an exception is thrown invoking the [ComputePropertyValueCallback], + /// [value] returns null and the exception thrown can be found via the + /// [exception] property. + /// + /// See also: + /// + /// * [valueToString], which converts the property value to a string. + @override + T get value { + _maybeCacheValue(); + return _value; + } + + T _value; + + bool _valueComputed; + + Object _exception; + + /// Exception thrown if accessing the property [value] threw an exception. + /// + /// Returns null if computing the property value did not throw an exception. + Object get exception { + _maybeCacheValue(); + return _exception; + } + + void _maybeCacheValue() { + if (_valueComputed) return; + + _valueComputed = true; + assert(_computeValue != null); + try { + _value = _computeValue(); + } catch (exception) { + _exception = exception; + _value = null; + } + } + + /// If the [value] of the property equals [defaultValue] the priority [level] + /// of the property is downgraded to [DiagnosticLevel.fine] as the property + /// value is uninteresting. + /// + /// [defaultValue] has type [T] or is [kNoDefaultValue]. + final Object defaultValue; + + final DiagnosticLevel _defaultLevel; + + /// Priority level of the diagnostic used to control which diagnostics should + /// be shown and filtered. + /// + /// The property level defaults to the value specified by the `level` + /// constructor argument. The level is raised to [DiagnosticLevel.error] if + /// an [exception] was thrown getting the property [value]. The level is + /// raised to [DiagnosticLevel.warning] if the property [value] is null and + /// the property is not allowed to be null due to [missingIfNull]. The + /// priority level is lowered to [DiagnosticLevel.fine] if the property + /// [value] equals [defaultValue]. + @override + DiagnosticLevel get level { + if (_defaultLevel == DiagnosticLevel.hidden) return _defaultLevel; + + if (exception != null) return DiagnosticLevel.error; + + if (value == null && missingIfNull) return DiagnosticLevel.warning; + + // Use a low level when the value matches the default value. + if (defaultValue != kNoDefaultValue && value == defaultValue) + return DiagnosticLevel.fine; + + return _defaultLevel; + } + + final ComputePropertyValueCallback _computeValue; + + @override + List getProperties() { + if (expandableValue) { + final T object = value; + if (object is DiagnosticsNode) { + return object.getProperties(); + } + if (object is DiagnosticableMixin) { + return object.toDiagnosticsNode(style: style).getProperties(); + } + } + return const []; + } + + @override + List getChildren() { + if (expandableValue) { + final T object = value; + if (object is DiagnosticsNode) { + return object.getChildren(); + } + if (object is DiagnosticableMixin) { + return object.toDiagnosticsNode(style: style).getChildren(); + } + } + return const []; + } +} + +/// [DiagnosticsNode] that lazily calls the associated [Diagnosticable] [value] +/// to implement [getChildren] and [getProperties]. +class DiagnosticableNode + extends DiagnosticsNode { + /// Create a diagnostics describing a [DiagnosticableMixin] value. + /// + /// The [value] argument must not be null. + DiagnosticableNode({ + String name, + @required this.value, + @required DiagnosticsTreeStyle style, + }) : assert(value != null), + super( + name: name, + style: style, + ); + + @override + final T value; + + DiagnosticPropertiesBuilder _cachedBuilder; + + /// Retrieve the [DiagnosticPropertiesBuilder] of current node. + /// + /// It will cache the result to prevent duplicate operation. + DiagnosticPropertiesBuilder get builder { + if (kReleaseMode) return null; + if (_cachedBuilder == null) { + _cachedBuilder = DiagnosticPropertiesBuilder(); + value?.debugFillProperties(_cachedBuilder); + } + return _cachedBuilder; + } + + @override + DiagnosticsTreeStyle get style { + return kReleaseMode + ? DiagnosticsTreeStyle.none + : super.style ?? builder.defaultDiagnosticsTreeStyle; + } + + @override + String get emptyBodyDescription => + kReleaseMode ? '' : builder.emptyBodyDescription; + + @override + List getProperties() => + kReleaseMode ? const [] : builder.properties; + + @override + List getChildren() { + return const []; + } + + @override + String toDescription({TextTreeConfiguration parentConfiguration}) { + if (kReleaseMode) { + return ''; + } + return value.toStringShort(); + } +} + +/// [DiagnosticsNode] for an instance of [DiagnosticableTree]. +class DiagnosticableTreeNode extends DiagnosticableNode { + /// Creates a [DiagnosticableTreeNode]. + DiagnosticableTreeNode({ + String name, + @required DiagnosticableTree value, + @required DiagnosticsTreeStyle style, + }) : super( + name: name, + value: value, + style: style, + ); + + @override + List getChildren() { + if (value != null) return value.debugDescribeChildren(); + return const []; + } +} + +/// Returns a 5 character long hexadecimal string generated from +/// [Object.hashCode]'s 20 least-significant bits. +String shortHash(Object object) { + return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); +} + +/// Returns a summary of the runtime type and hash code of `object`. +/// +/// See also: +/// +/// * [Object.hashCode], a value used when placing an object in a [Map] or +/// other similar data structure, and which is also used in debug output to +/// distinguish instances of the same class (hash collisions are +/// possible, but rare enough that its use in debug output is useful). +/// * [Object.runtimeType], the [Type] of an object. +String describeIdentity(Object object) => + '${object.runtimeType}#${shortHash(object)}'; + +// This method exists as a workaround for https://github.com/dart-lang/sdk/issues/30021 +/// Returns a short description of an enum value. +/// +/// Strips off the enum class name from the `enumEntry.toString()`. +/// +/// {@tool sample} +/// +/// ```dart +/// enum Day { +/// monday, tuesday, wednesday, thursday, friday, saturday, sunday +/// } +/// +/// void validateDescribeEnum() { +/// assert(Day.monday.toString() == 'Day.monday'); +/// assert(describeEnum(Day.monday) == 'monday'); +/// } +/// ``` +/// {@end-tool} +String describeEnum(Object enumEntry) { + final String description = enumEntry.toString(); + final int indexOfDot = description.indexOf('.'); + assert(indexOfDot != -1 && indexOfDot < description.length - 1); + return description.substring(indexOfDot + 1); +} + +/// Builder to accumulate properties and configuration used to assemble a +/// [DiagnosticsNode] from a [DiagnosticableMixin] object. +class DiagnosticPropertiesBuilder { + /// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to + /// an empty array. + DiagnosticPropertiesBuilder() : properties = []; + + /// Creates a [DiagnosticPropertiesBuilder] with a given [properties]. + DiagnosticPropertiesBuilder.fromProperties(this.properties); + + /// Add a property to the list of properties. + void add(DiagnosticsNode property) { + if (!kReleaseMode) { + properties.add(property); + } + } + + /// List of properties accumulated so far. + final List properties; + + /// Default style to use for the [DiagnosticsNode] if no style is specified. + DiagnosticsTreeStyle defaultDiagnosticsTreeStyle = + DiagnosticsTreeStyle.sparse; + + /// Description to show if the node has no displayed properties or children. + String emptyBodyDescription; +} + +// Examples can assume: +// class ExampleSuperclass with DiagnosticableMixin { String message; double stepWidth; double scale; double paintExtent; double hitTestExtent; double paintExtend; double maxWidth; bool primary; double progress; int maxLines; Duration duration; int depth; dynamic boxShadow; dynamic style; bool hasSize; Matrix4 transform; Map handles; Color color; bool obscureText; ImageRepeat repeat; Size size; Widget widget; bool isCurrent; bool keepAlive; TextAlign textAlign; } + +/// A base class for providing string and [DiagnosticsNode] debug +/// representations describing the properties of an object. +/// +/// The string debug representation is generated from the intermediate +/// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is +/// also used by debugging tools displaying interactive trees of objects and +/// properties. +/// +/// See also: +/// +/// * [DiagnosticableTree], which extends this class to also describe the +/// children of a tree structured object. +/// * [DiagnosticableMixin], which provides the implementation for +/// [Diagnosticable], and can be used to add diagnostics to classes which +/// already have a base class. +/// * [DiagnosticableMixin.debugFillProperties], which lists best practices +/// for specifying the properties of a [DiagnosticsNode]. The most common use +/// case is to override [debugFillProperties] defining custom properties for +/// a subclass of [DiagnosticableTreeMixin] using the existing +/// [DiagnosticsProperty] subclasses. +/// * [DiagnosticableTree.debugDescribeChildren], which lists best practices +/// for describing the children of a [DiagnosticsNode]. Typically the base +/// class already describes the children of a node properly or a node has +/// no children. +/// * [DiagnosticsProperty], which should be used to create leaf diagnostic +/// nodes without properties or children. There are many +/// [DiagnosticsProperty] subclasses to handle common use cases. +abstract class Diagnosticable with DiagnosticableMixin { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const Diagnosticable(); +} + +/// A mixin class that provides the implementation for [Diagnosticable]. +/// +/// This mixin can be used to add diagnostics to a class which already has an +/// base class. +/// +/// The string debug representation is generated from the intermediate +/// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is +/// also used by debugging tools displaying interactive trees of objects and +/// properties. +/// +/// See also: +/// +/// * [debugFillProperties], which lists best practices for specifying the +/// properties of a [DiagnosticsNode]. The most common use case is to +/// override [debugFillProperties] defining custom properties for a subclass +/// of [DiagnosticableTreeMixin] using the existing [DiagnosticsProperty] +/// subclasses. +/// * [DiagnosticableTree], which extends this class to also describe the +/// children of a tree structured object. +/// * [DiagnosticableTree.debugDescribeChildren], which lists best practices +/// for describing the children of a [DiagnosticsNode]. Typically the base +/// class already describes the children of a node properly or a node has +/// no children. +/// * [DiagnosticsProperty], which should be used to create leaf diagnostic +/// nodes without properties or children. There are many +/// [DiagnosticsProperty] subclasses to handle common use cases. +mixin DiagnosticableMixin { + /// A brief description of this object, usually just the [runtimeType] and the + /// [hashCode]. + /// + /// See also: + /// + /// * [toString], for a detailed description of the object. + String toStringShort() => describeIdentity(this); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) { + String fullString; + assert(() { + fullString = toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine) + .toString(minLevel: minLevel); + return true; + }()); + return fullString ?? toStringShort(); + } + + /// Returns a debug representation of the object that is used by debugging + /// tools and by [DiagnosticsNode.toStringDeep]. + /// + /// Leave [name] as null if there is not a meaningful description of the + /// relationship between the this node and its parent. + /// + /// Typically the [style] argument is only specified to indicate an atypical + /// relationship between the parent and the node. For example, pass + /// [DiagnosticsTreeStyle.offstage] to indicate that a node is offstage. + DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) { + return DiagnosticableNode( + name: name, + value: this, + style: style, + ); + } + + /// Add additional properties associated with the node. + /// + /// Use the most specific [DiagnosticsProperty] existing subclass to describe + /// each property instead of the [DiagnosticsProperty] base class. There are + /// only a small number of [DiagnosticsProperty] subclasses each covering a + /// common use case. Consider what values a property is relevant for users + /// debugging as users debugging large trees are overloaded with information. + /// Common named parameters in [DiagnosticsNode] subclasses help filter when + /// and how properties are displayed. + /// + /// `defaultValue`, `showName`, `showSeparator`, and `level` keep string + /// representations of diagnostics terse and hide properties when they are not + /// very useful. + /// + /// * Use `defaultValue` any time the default value of a property is + /// uninteresting. For example, specify a default value of null any time + /// a property being null does not indicate an error. + /// * Avoid specifying the `level` parameter unless the result you want + /// cannot be achieved by using the `defaultValue` parameter or using + /// the [ObjectFlagProperty] class to conditionally display the property + /// as a flag. + /// * Specify `showName` and `showSeparator` in rare cases where the string + /// output would look clumsy if they were not set. + /// ```dart + /// DiagnosticsProperty('child(3, 4)', null, ifNull: 'is null', showSeparator: false).toString() + /// ``` + /// Shows using `showSeparator` to get output `child(3, 4) is null` which + /// is more polished than `child(3, 4): is null`. + /// ```dart + /// DiagnosticsProperty('icon', icon, ifNull: '', showName: false)).toString() + /// ``` + /// Shows using `showName` to omit the property name as in this context the + /// property name does not add useful information. + /// + /// `ifNull`, `ifEmpty`, `unit`, and `tooltip` make property + /// descriptions clearer. The examples in the code sample below illustrate + /// good uses of all of these parameters. + /// + /// ## DiagnosticsProperty subclasses for primitive types + /// + /// * [StringProperty], which supports automatically enclosing a [String] + /// value in quotes. + /// * [DoubleProperty], which supports specifying a unit of measurement for + /// a [double] value. + /// * [PercentProperty], which clamps a [double] to between 0 and 1 and + /// formats it as a percentage. + /// * [IntProperty], which supports specifying a unit of measurement for an + /// [int] value. + /// * [FlagProperty], which formats a [bool] value as one or more flags. + /// Depending on the use case it is better to format a bool as + /// `DiagnosticsProperty` instead of using [FlagProperty] as the + /// output is more verbose but unambiguous. + /// + /// ## Other important [DiagnosticsProperty] variants + /// + /// * [EnumProperty], which provides terse descriptions of enum values + /// working around limitations of the `toString` implementation for Dart + /// enum types. + /// * [IterableProperty], which handles iterable values with display + /// customizable depending on the [DiagnosticsTreeStyle] used. + /// * [ObjectFlagProperty], which provides terse descriptions of whether a + /// property value is present or not. For example, whether an `onClick` + /// callback is specified or an animation is in progress. + /// * [ColorProperty], which must be used if the property value is + /// a [Color] or one of its subclasses. + /// * [IconDataProperty], which must be used if the property value + /// is of type [IconData]. + /// + /// If none of these subclasses apply, use the [DiagnosticsProperty] + /// constructor or in rare cases create your own [DiagnosticsProperty] + /// subclass as in the case for [TransformProperty] which handles [Matrix4] + /// that represent transforms. Generally any property value with a good + /// `toString` method implementation works fine using [DiagnosticsProperty] + /// directly. + /// + /// {@tool sample} + /// + /// This example shows best practices for implementing [debugFillProperties] + /// illustrating use of all common [DiagnosticsProperty] subclasses and all + /// common [DiagnosticsProperty] parameters. + /// + /// ```dart + /// class ExampleObject extends ExampleSuperclass { + /// + /// // ...various members and properties... + /// + /// @override + /// void debugFillProperties(DiagnosticPropertiesBuilder properties) { + /// // Always add properties from the base class first. + /// super.debugFillProperties(properties); + /// + /// // Omit the property name 'message' when displaying this String property + /// // as it would just add visual noise. + /// properties.add(StringProperty('message', message, showName: false)); + /// + /// properties.add(DoubleProperty('stepWidth', stepWidth)); + /// + /// // A scale of 1.0 does nothing so should be hidden. + /// properties.add(DoubleProperty('scale', scale, defaultValue: 1.0)); + /// + /// // If the hitTestExtent matches the paintExtent, it is just set to its + /// // default value so is not relevant. + /// properties.add(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent)); + /// + /// // maxWidth of double.infinity indicates the width is unconstrained and + /// // so maxWidth has no impact., + /// properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity)); + /// + /// // Progress is a value between 0 and 1 or null. Showing it as a + /// // percentage makes the meaning clear enough that the name can be + /// // hidden. + /// properties.add(PercentProperty( + /// 'progress', + /// progress, + /// showName: false, + /// ifNull: '', + /// )); + /// + /// // Most text fields have maxLines set to 1. + /// properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + /// + /// // Specify the unit as otherwise it would be unclear that time is in + /// // milliseconds. + /// properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms')); + /// + /// // Tooltip is used instead of unit for this case as a unit should be a + /// // terse description appropriate to display directly after a number + /// // without a space. + /// properties.add(DoubleProperty( + /// 'device pixel ratio', + /// ui.window.devicePixelRatio, + /// tooltip: 'physical pixels per logical pixel', + /// )); + /// + /// // Displaying the depth value would be distracting. Instead only display + /// // if the depth value is missing. + /// properties.add(ObjectFlagProperty('depth', depth, ifNull: 'no depth')); + /// + /// // bool flag that is only shown when the value is true. + /// properties.add(FlagProperty('using primary controller', value: primary)); + /// + /// properties.add(FlagProperty( + /// 'isCurrent', + /// value: isCurrent, + /// ifTrue: 'active', + /// ifFalse: 'inactive', + /// showName: false, + /// )); + /// + /// properties.add(DiagnosticsProperty('keepAlive', keepAlive)); + /// + /// // FlagProperty could have also been used in this case. + /// // This option results in the text "obscureText: true" instead + /// // of "obscureText" which is a bit more verbose but a bit clearer. + /// properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); + /// + /// properties.add(EnumProperty('textAlign', textAlign, defaultValue: null)); + /// properties.add(EnumProperty('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); + /// + /// // Warn users when the widget is missing but do not show the value. + /// properties.add(ObjectFlagProperty('widget', widget, ifNull: 'no widget')); + /// + /// properties.add(IterableProperty( + /// 'boxShadow', + /// boxShadow, + /// defaultValue: null, + /// style: style, + /// )); + /// + /// // Getting the value of size throws an exception unless hasSize is true. + /// properties.add(DiagnosticsProperty.lazy( + /// 'size', + /// () => size, + /// description: '${ hasSize ? size : "MISSING" }', + /// )); + /// + /// // If the `toString` method for the property value does not provide a + /// // good terse description, write a DiagnosticsProperty subclass as in + /// // the case of TransformProperty which displays a nice debugging view + /// // of a Matrix4 that represents a transform. + /// properties.add(TransformProperty('transform', transform)); + /// + /// // If the value class has a good `toString` method, use + /// // DiagnosticsProperty. Specifying the value type ensures + /// // that debugging tools always know the type of the field and so can + /// // provide the right UI affordances. For example, in this case even + /// // if color is null, a debugging tool still knows the value is a Color + /// // and can display relevant color related UI. + /// properties.add(DiagnosticsProperty('color', color)); + /// + /// // Use a custom description to generate a more terse summary than the + /// // `toString` method on the map class. + /// properties.add(DiagnosticsProperty>( + /// 'handles', + /// handles, + /// description: handles != null ? + /// '${handles.length} active client${ handles.length == 1 ? "" : "s" }' : + /// null, + /// ifNull: 'no notifications ever received', + /// showName: false, + /// )); + /// } + /// } + /// ``` + /// {@end-tool} + /// + /// Used by [toDiagnosticsNode] and [toString]. + @protected + @mustCallSuper + void debugFillProperties(DiagnosticPropertiesBuilder properties) {} +} + +/// A base class for providing string and [DiagnosticsNode] debug +/// representations describing the properties and children of an object. +/// +/// The string debug representation is generated from the intermediate +/// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is +/// also used by debugging tools displaying interactive trees of objects and +/// properties. +/// +/// See also: +/// +/// * [DiagnosticableTreeMixin], a mixin that implements this class. +/// * [Diagnosticable], which should be used instead of this class to provide +/// diagnostics for objects without children. +abstract class DiagnosticableTree extends Diagnosticable { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const DiagnosticableTree(); + + /// Returns a one-line detailed description of the object. + /// + /// This description is often somewhat long. This includes the same + /// information given by [toStringDeep], but does not recurse to any children. + /// + /// `joiner` specifies the string which is place between each part obtained + /// from [debugFillProperties]. Passing a string such as `'\n '` will result + /// in a multi-line string that indents the properties of the object below its + /// name (as per [toString]). + /// + /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included + /// in the output. + /// + /// See also: + /// + /// * [toString], for a brief description of the object. + /// * [toStringDeep], for a description of the subtree rooted at this object. + String toStringShallow({ + String joiner = ', ', + DiagnosticLevel minLevel = DiagnosticLevel.debug, + }) { + if (kReleaseMode) { + return toString(); + } + final StringBuffer result = StringBuffer(); + result.write(toString()); + result.write(joiner); + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + debugFillProperties(builder); + result.write( + builder.properties + .where((DiagnosticsNode n) => !n.isFiltered(minLevel)) + .join(joiner), + ); + return result.toString(); + } + + /// Returns a string representation of this node and its descendants. + /// + /// `prefixLineOne` will be added to the front of the first line of the + /// output. `prefixOtherLines` will be added to the front of each other line. + /// If `prefixOtherLines` is null, the `prefixLineOne` is used for every line. + /// By default, there is no prefix. + /// + /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included + /// in the output. + /// + /// The [toStringDeep] method takes other arguments, but those are intended + /// for internal use when recursing to the descendants, and so can be ignored. + /// + /// See also: + /// + /// * [toString], for a brief description of the object but not its children. + /// * [toStringShallow], for a detailed description of the object but not its + /// children. + String toStringDeep({ + String prefixLineOne = '', + String prefixOtherLines, + DiagnosticLevel minLevel = DiagnosticLevel.debug, + }) { + return toDiagnosticsNode().toStringDeep( + prefixLineOne: prefixLineOne, + prefixOtherLines: prefixOtherLines, + minLevel: minLevel); + } + + @override + String toStringShort() => describeIdentity(this); + + @override + DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) { + return DiagnosticableTreeNode( + name: name, + value: this, + style: style, + ); + } + + /// Returns a list of [DiagnosticsNode] objects describing this node's + /// children. + /// + /// Children that are offstage should be added with `style` set to + /// [DiagnosticsTreeStyle.offstage] to indicate that they are offstage. + /// + /// The list must not contain any null entries. If there are explicit null + /// children to report, consider [new DiagnosticsNode.message] or + /// [DiagnosticsProperty] as possible [DiagnosticsNode] objects to + /// provide. + /// + /// Used by [toStringDeep], [toDiagnosticsNode] and [toStringShallow]. + /// + /// See also: + /// + /// * [RenderTable.debugDescribeChildren], which provides high quality custom + /// descriptions for its child nodes. + @protected + List debugDescribeChildren() => const []; +} + +/// A mixin that helps dump string and [DiagnosticsNode] representations of trees. +/// +/// This mixin is identical to class [DiagnosticableTree]. +mixin DiagnosticableTreeMixin implements DiagnosticableTree { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) { + return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine) + .toString(minLevel: minLevel); + } + + @override + String toStringShallow({ + String joiner = ', ', + DiagnosticLevel minLevel = DiagnosticLevel.debug, + }) { + if (kReleaseMode) { + return toString(); + } + final StringBuffer result = StringBuffer(); + result.write(toStringShort()); + result.write(joiner); + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + debugFillProperties(builder); + result.write( + builder.properties + .where((DiagnosticsNode n) => !n.isFiltered(minLevel)) + .join(joiner), + ); + return result.toString(); + } + + @override + String toStringDeep({ + String prefixLineOne = '', + String prefixOtherLines, + DiagnosticLevel minLevel = DiagnosticLevel.debug, + }) { + return toDiagnosticsNode().toStringDeep( + prefixLineOne: prefixLineOne, + prefixOtherLines: prefixOtherLines, + minLevel: minLevel); + } + + @override + String toStringShort() => describeIdentity(this); + + @override + DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) { + return DiagnosticableTreeNode( + name: name, + value: this, + style: style, + ); + } + + @override + List debugDescribeChildren() => const []; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) {} +} + +/// [DiagnosticsNode] that exists mainly to provide a container for other +/// diagnostics that typically lacks a meaningful value of its own. +/// +/// This class is typically used for displaying complex nested error messages. +class DiagnosticsBlock extends DiagnosticsNode { + /// Creates a diagnostic with properties specified by [properties] and + /// children specified by [children]. + DiagnosticsBlock({ + String name, + DiagnosticsTreeStyle style = DiagnosticsTreeStyle.whitespace, + bool showName = true, + bool showSeparator = true, + String linePrefix, + this.value, + String description, + this.level = DiagnosticLevel.info, + this.allowTruncate = false, + List children = const [], + List properties = const [], + }) : _description = description, + _children = children, + _properties = properties, + super( + name: name, + style: style, + showName: showName && name != null, + showSeparator: showSeparator, + linePrefix: linePrefix, + ); + + final List _children; + final List _properties; + + @override + final DiagnosticLevel level; + final String _description; + @override + final Object value; + + @override + final bool allowTruncate; + + @override + List getChildren() => _children; + + @override + List getProperties() => _properties; + + @override + String toDescription({TextTreeConfiguration parentConfiguration}) => + _description; +} + +/// A delegate that configures how a hierarchy of [DiagnosticsNode]s should be +/// serialized. +/// +/// Implement this class in a subclass to fully configure how [DiagnosticsNode]s +/// get serialized. +abstract class DiagnosticsSerializationDelegate { + /// Creates a simple [DiagnosticsSerializationDelegate] that controls the + /// [subtreeDepth] and whether to [includeProperties]. + /// + /// For additional configuration options, extend + /// [DiagnosticsSerializationDelegate] and provide custom implementations + /// for the methods of this class. + const factory DiagnosticsSerializationDelegate({ + int subtreeDepth, + bool includeProperties, + }) = _DefaultDiagnosticsSerializationDelegate; + + /// Returns a serializable map of additional information that will be included + /// in the serialization of the given [DiagnosticsNode]. + /// + /// This method is called for every [DiagnosticsNode] that's included in + /// the serialization. + Map additionalNodeProperties(DiagnosticsNode node); + + /// Filters the list of [DiagnosticsNode]s that will be included as children + /// for the given `owner` node. + /// + /// The callback may return a subset of the children in the provided list + /// or replace the entire list with new child nodes. + /// + /// See also: + /// + /// * [subtreeDepth], which controls how many levels of children will be + /// included in the serialization. + List filterChildren( + List nodes, DiagnosticsNode owner); + + /// Filters the list of [DiagnosticsNode]s that will be included as properties + /// for the given `owner` node. + /// + /// The callback may return a subset of the properties in the provided list + /// or replace the entire list with new property nodes. + /// + /// By default, `nodes` is returned as-is. + /// + /// See also: + /// + /// * [includeProperties], which controls whether properties will be included + /// at all. + List filterProperties( + List nodes, DiagnosticsNode owner); + + /// Truncates the given list of [DiagnosticsNode] that will be added to the + /// serialization as children or properties of the `owner` node. + /// + /// The method must return a subset of the provided nodes and may + /// not replace any nodes. While [filterProperties] and [filterChildren] + /// completely hide a node from the serialization, truncating a node will + /// leave a hint in the serialization that there were additional nodes in the + /// result that are not included in the current serialization. + /// + /// By default, `nodes` is returned as-is. + List truncateNodesList( + List nodes, DiagnosticsNode owner); + + /// Returns the [DiagnosticsSerializationDelegate] to be used + /// for adding the provided [DiagnosticsNode] to the serialization. + /// + /// By default, this will return a copy of this delegate, which has the + /// [subtreeDepth] reduced by one. + /// + /// This is called for nodes that will be added to the serialization as + /// property or child of another node. It may return the same delegate if no + /// changes to it are necessary. + DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node); + + /// Controls how many levels of children will be included in the serialized + /// hierarchy of [DiagnosticsNode]s. + /// + /// Defaults to zero. + /// + /// See also: + /// + /// * [filterChildren], which provides a way to filter the children that + /// will be included. + int get subtreeDepth; + + /// Whether to include the properties of a [DiagnosticsNode] in the + /// serialization. + /// + /// Defaults to false. + /// + /// See also: + /// + /// * [filterProperties], which provides a way to filter the properties that + /// will be included. + bool get includeProperties; + + /// Whether properties that have a [Diagnosticable] as value should be + /// expanded. + bool get expandPropertyValues; + + /// Creates a copy of this [DiagnosticsSerializationDelegate] with the + /// provided values. + DiagnosticsSerializationDelegate copyWith({ + int subtreeDepth, + bool includeProperties, + }); +} + +class _DefaultDiagnosticsSerializationDelegate + implements DiagnosticsSerializationDelegate { + const _DefaultDiagnosticsSerializationDelegate({ + this.includeProperties = false, + this.subtreeDepth = 0, + }); + + @override + Map additionalNodeProperties(DiagnosticsNode node) { + return const {}; + } + + @override + DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) { + return subtreeDepth > 0 ? copyWith(subtreeDepth: subtreeDepth - 1) : this; + } + + @override + bool get expandPropertyValues => false; + + @override + List filterChildren( + List nodes, DiagnosticsNode owner) { + return nodes; + } + + @override + List filterProperties( + List nodes, DiagnosticsNode owner) { + return nodes; + } + + @override + final bool includeProperties; + + @override + final int subtreeDepth; + + @override + List truncateNodesList( + List nodes, DiagnosticsNode owner) { + return nodes; + } + + @override + DiagnosticsSerializationDelegate copyWith( + {int subtreeDepth, bool includeProperties}) { + return _DefaultDiagnosticsSerializationDelegate( + subtreeDepth: subtreeDepth ?? this.subtreeDepth, + includeProperties: includeProperties ?? this.includeProperties, + ); + } +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/key.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/key.dart new file mode 100644 index 00000000000..2de76be3470 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/key.dart @@ -0,0 +1,19 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +abstract class Key { + const factory Key(String value) = ValueKey; + + const Key._(); +} + +abstract class LocalKey extends Key { + const LocalKey() : super._(); +} + +class ValueKey extends LocalKey { + final T value; + + const ValueKey(this.value); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/platform.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/platform.dart new file mode 100644 index 00000000000..e8b5e579f85 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/foundation/platform.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +TargetPlatform get defaultTargetPlatform => null; + +enum TargetPlatform { + android, + fuchsia, + iOS, + linux, + macOS, + windows, +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/material/button.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/material/button.dart new file mode 100644 index 00000000000..1603ba8d291 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/material/button.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +class RawMaterialButton extends StatefulWidget { + final Widget? child; + final VoidCallback? onPressed; + + const RawMaterialButton({ Key? key, required this.onPressed, this.child }); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/material/scaffold.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/material/scaffold.dart new file mode 100644 index 00000000000..181ae905b53 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/material/scaffold.dart @@ -0,0 +1,12 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +class Scaffold extends StatefulWidget { + const Scaffold({ + Key key, + Widget body, + }); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/painting/box_decoration.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/painting/box_decoration.dart new file mode 100644 index 00000000000..881503b9003 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/painting/box_decoration.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +class BoxDecoration {} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/flex.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/flex.dart new file mode 100644 index 00000000000..d1c1e8e42f1 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/flex.dart @@ -0,0 +1,25 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +enum CrossAxisAlignment { + start, + end, + center, + stretch, + baseline, +} + +enum MainAxisAlignment { + start, + end, + center, + spaceBetween, + spaceAround, + spaceEvenly, +} + +enum MainAxisSize { + min, + max, +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/object.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/object.dart new file mode 100644 index 00000000000..f6ea790996e --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/rendering/object.dart @@ -0,0 +1,2 @@ +abstract class RenderObject //extends AbstractNode with DiagnosticableTreeMixin +{} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/basic.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/basic.dart new file mode 100644 index 00000000000..72daf6d99b9 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/basic.dart @@ -0,0 +1,142 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; + +export 'package:flutter/rendering.dart'; + +class Align extends SingleChildRenderObjectWidget { + /// How to align the child. + /// + /// The x and y values of the [Alignment] control the horizontal and vertical + /// alignment, respectively. An x value of -1.0 means that the left edge of + /// the child is aligned with the left edge of the parent whereas an x value + /// of 1.0 means that the right edge of the child is aligned with the right + /// edge of the parent. Other values interpolate (and extrapolate) linearly. + /// For example, a value of 0.0 means that the center of the child is aligned + /// with the center of the parent. + /// + /// See also: + /// + /// * [Alignment], which has more details and some convenience constants for + /// common positions. + /// * [AlignmentDirectional], which has a horizontal coordinate orientation + /// that depends on the [TextDirection]. + final AlignmentGeometry alignment; + + /// If non-null, sets its width to the child's width multiplied by this factor. + /// + /// Can be both greater and less than 1.0 but must be positive. + final double widthFactor; + + /// If non-null, sets its height to the child's height multiplied by this factor. + /// + /// Can be both greater and less than 1.0 but must be positive. + final double heightFactor; + + /// Creates an alignment widget. + /// + /// The alignment defaults to [Alignment.center]. + const Align({ + Key key, + this.alignment = Alignment.center, + this.widthFactor, + this.heightFactor, + Widget child, + }) : assert(alignment != null), + assert(widthFactor == null || widthFactor >= 0.0), + assert(heightFactor == null || heightFactor >= 0.0), + super(key: key, child: child); +} + +class AspectRatio extends SingleChildRenderObjectWidget { + const AspectRatio({ + Key key, + @required double aspectRatio, + Widget child, + }); +} + +class Center extends StatelessWidget { + const Center({Key key, double heightFactor, Widget child}); +} + +class ClipRect extends SingleChildRenderObjectWidget { + const ClipRect({Key key, Widget child}) : super(key: key, child: child); + + /// Does not actually exist in Flutter. + const ClipRect.rect({Key key, Widget child}) : super(key: key, child: child); +} + +class Column extends Flex { + Column({ + Key key, + MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + MainAxisSize mainAxisSize = MainAxisSize.max, + CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, + TextDirection textDirection, + VerticalDirection verticalDirection = VerticalDirection.down, + TextBaseline textBaseline, + List children = const [], + }); +} + +class Expanded extends StatelessWidget { + const Expanded({ + Key key, + int flex = 1, + @required Widget child, + }); +} + +class Flex extends Widget { + Flex({ + Key key, + List children = const [], + }); +} + +class Padding extends SingleChildRenderObjectWidget { + final EdgeInsetsGeometry padding; + + const Padding({ + Key key, + @required this.padding, + Widget child, + }); +} + +class Row extends Flex { + Row({ + Key key, + MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + MainAxisSize mainAxisSize = MainAxisSize.max, + CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, + TextDirection textDirection, + VerticalDirection verticalDirection = VerticalDirection.down, + TextBaseline textBaseline, + List children = const [], + }); +} + +class SizedBox extends SingleChildRenderObjectWidget { + const SizedBox({Key? key, double? width, double? height, Widget? child}); + const SizedBox.expand({Key? key, Widget? child}); + const SizedBox.shrink({Key? key, Widget? child}); + SizedBox.fromSize({Key? key, Widget? child, Size? size}); + const SizedBox.square({Key? key, Widget? child, double? dimension}); +} + +class Transform extends SingleChildRenderObjectWidget { + const Transform({ + Key key, + @required transform, + origin, + alignment, + transformHitTests = true, + Widget child, + }); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/container.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/container.dart new file mode 100644 index 00000000000..87b7a06938a --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/container.dart @@ -0,0 +1,66 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../foundation.dart'; +import 'framework.dart'; + +class Container extends StatelessWidget { + final Widget child; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry padding; + + /// The decoration to paint behind the [child]. + /// + /// A shorthand for specifying just a solid color is available in the + /// constructor: set the `color` argument instead of the `decoration` + /// argument. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration decoration; + + /// The decoration to paint in front of the [child]. + final Decoration foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry margin; + + Container({ + Key key, + this.alignment, + this.padding, + Color color, + Decoration decoration, + this.foregroundDecoration, + double width, + double height, + this.margin, + this.child, + }) : decoration = decoration, + super(key: key); + + @override + Widget build(BuildContext context) => child; +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/framework.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/framework.dart new file mode 100644 index 00000000000..0fb8f85bd21 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/framework.dart @@ -0,0 +1,56 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../foundation.dart'; +import '../rendering/object.dart'; + +typedef void VoidCallback(); + +abstract class BuildContext { + Widget get widget; + bool get mounted; + RenderObject? findRenderObject(); +} + +abstract class State { + BuildContext get context => null; + + bool get mounted => false; + + T get widget => null; + + Widget build(BuildContext context) => null; + + void dispose() {} + + void setState(VoidCallback fn) {} +} + +abstract class StatefulWidget extends Widget { + const StatefulWidget({Key key}) : super(key: key); + + State createState() => null; +} + +abstract class StatelessWidget extends Widget { + const StatelessWidget({Key key}) : super(key: key); + + Widget build(BuildContext context) => null; +} + +class Widget { + final Key key; + + const Widget({this.key}); +} + +abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { + final Widget child; + + const SingleChildRenderObjectWidget({Key key, this.child}) : super(key: key); +} + +abstract class RenderObjectWidget extends Widget { + const RenderObjectWidget({Key key}) : super(key: key); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/navigator.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/navigator.dart new file mode 100644 index 00000000000..caceda61fe2 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/navigator.dart @@ -0,0 +1,124 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + +class Navigator extends StatefulWidget { + static NavigatorState of( + BuildContext context, { + bool rootNavigator = false, + bool nullOk = false, + }) => + null; + + @optionalTypeArgs + static Future pushNamed( + BuildContext context, + String routeName, { + Object? arguments, + }) => + null; + + @optionalTypeArgs + static Future pushReplacementNamed( + BuildContext context, + String routeName, { + TO result, + Object arguments, + }) => + null; + + @optionalTypeArgs + static Future pushNamedAndRemoveUntil( + BuildContext context, + String newRouteName, + RoutePredicate predicate, { + Object arguments, + }) => + null; + + @optionalTypeArgs + static Future push( + BuildContext context, Route route) => + null; + @optionalTypeArgs + static Future pushReplacement( + BuildContext context, Route newRoute, + {TO result}) => + null; + + @optionalTypeArgs + static Future pushAndRemoveUntil( + BuildContext context, Route newRoute, RoutePredicate predicate) => + null; + + @optionalTypeArgs + static Future maybePop(BuildContext context, + [T result]) => + null; + + @optionalTypeArgs + static Future popAndPushNamed( + BuildContext context, + String routeName, { + TO result, + Object arguments, + }) => + null; +} + +class NavigatorState extends State { + @optionalTypeArgs + Future pushNamed( + String routeName, { + Object arguments, + }) => + null; + + @optionalTypeArgs + Future pushReplacementNamed( + String routeName, { + TO result, + Object arguments, + }) => + null; + + @optionalTypeArgs + Future popAndPushNamed( + String routeName, { + TO result, + Object arguments, + }) => + null; + + @optionalTypeArgs + Future pushNamedAndRemoveUntil( + String newRouteName, + RoutePredicate predicate, { + Object arguments, + }) => + null; + + @optionalTypeArgs + Future push(Route route) => null; + + @optionalTypeArgs + Future pushReplacement( + Route newRoute, + {TO result}) => + null; + + @optionalTypeArgs + Future pushAndRemoveUntil( + Route newRoute, RoutePredicate predicate) => + null; + + @optionalTypeArgs + Future maybePop([T result]) async => null; +} + +typedef RoutePredicate = bool Function(Route route); + +abstract class Route {} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/placeholder.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/placeholder.dart new file mode 100644 index 00000000000..67707d95c83 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/placeholder.dart @@ -0,0 +1,21 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'basic.dart'; +import 'framework.dart'; + +class Placeholder extends StatelessWidget { + final Color color; + final double strokeWidth; + final double fallbackWidth; + final double fallbackHeight; + + const Placeholder({ + Key key, + this.color = const Color(0xFF455A64), // Blue Grey 700 + this.strokeWidth = 2.0, + this.fallbackWidth = 400.0, + this.fallbackHeight = 400.0, + }) : super(key: key); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/text.dart b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/text.dart new file mode 100644 index 00000000000..a052b280db9 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/src/widgets/text.dart @@ -0,0 +1,113 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; + +import 'basic.dart'; +import 'framework.dart'; + +class DefaultTextStyle extends StatelessWidget { + DefaultTextStyle({Widget child}); +} + +class Text extends StatelessWidget { + /// The text to display. + /// + /// This will be null if a [textSpan] is provided instead. + final String data; + + /// If non-null, the style to use for this text. + /// + /// If the style's "inherit" property is true, the style will be merged with + /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will + /// replace the closest enclosing [DefaultTextStyle]. + final TextStyle style; + + /// How the text should be aligned horizontally. + final TextAlign textAlign; + + /// The directionality of the text. + /// + /// This decides how [textAlign] values like [TextAlign.start] and + /// [TextAlign.end] are interpreted. + /// + /// This is also used to disambiguate how to render bidirectional text. For + /// example, if the [data] is an English phrase followed by a Hebrew phrase, + /// in a [TextDirection.ltr] context the English phrase will be on the left + /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] + /// context, the English phrase will be on the right and the Hebrew phrase on + /// its left. + /// + /// Defaults to the ambient [Directionality], if any. + final TextDirection textDirection; + + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool softWrap; + + /// How visual overflow should be handled. + final TextOverflow overflow; + + /// The number of font pixels for each logical pixel. + /// + /// For example, if the text scale factor is 1.5, text will be 50% larger than + /// the specified font size. + /// + /// The value given to the constructor as textScaleFactor. If null, will + /// use the [MediaQueryData.textScaleFactor] obtained from the ambient + /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. + final double textScaleFactor; + + /// An optional maximum number of lines for the text to span, wrapping if necessary. + /// If the text exceeds the given number of lines, it will be truncated according + /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. + final int maxLines; + + /// An alternative semantics label for this text. + /// + /// If present, the semantics of this widget will contain this value instead + /// of the actual text. This will overwrite any of the semantics labels applied + /// directly to the [TextSpan]s. + /// + /// This is useful for replacing abbreviations or shorthands with the full + /// text value: + /// + /// ```dart + /// Text(r'$$', semanticsLabel: 'Double dollars') + /// ``` + final String semanticsLabel; + + /// {@macro flutter.dart:ui.text.TextWidthBasis} + final TextWidthBasis textWidthBasis; + + /// Creates a text widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + /// + /// The [data] parameter must not be null. + const Text( + this.data, { + Key key, + this.style, + this.textAlign, + this.textDirection, + this.softWrap, + this.overflow, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + this.textWidthBasis, + }) : super(key: key); +} diff --git a/pkg/linter/test_data/mock_packages/flutter/lib/widgets.dart b/pkg/linter/test_data/mock_packages/flutter/lib/widgets.dart new file mode 100644 index 00000000000..09ff51563d2 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/lib/widgets.dart @@ -0,0 +1,10 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/widgets/basic.dart'; +export 'src/widgets/container.dart'; +export 'src/widgets/framework.dart'; +export 'src/widgets/navigator.dart'; +export 'src/widgets/placeholder.dart'; +export 'src/widgets/text.dart'; diff --git a/pkg/linter/test_data/mock_packages/flutter/pubspec.yaml b/pkg/linter/test_data/mock_packages/flutter/pubspec.yaml new file mode 100644 index 00000000000..2ec2585b27e --- /dev/null +++ b/pkg/linter/test_data/mock_packages/flutter/pubspec.yaml @@ -0,0 +1,4 @@ +name: flutter + +environment: + sdk: '>=2.2.2 <3.0.0' diff --git a/pkg/linter/test_data/mock_packages/meta/lib/meta.dart b/pkg/linter/test_data/mock_packages/meta/lib/meta.dart new file mode 100644 index 00000000000..7a438faa5c3 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/meta/lib/meta.dart @@ -0,0 +1,340 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Constants for use in metadata annotations. +/// +/// See also `@deprecated` and `@override` in the `dart:core` library. +/// +/// Annotations provide semantic information that tools can use to provide a +/// better user experience. For example, an IDE might not autocomplete the name +/// of a function that's been marked `@deprecated`, or it might display the +/// function's name differently. +/// +/// For information on installing and importing this library, see the [meta +/// package on pub.dev](https://pub.dev/packages/meta). For examples of using +/// annotations, see +/// [Metadata](https://dart.dev/guides/language/language-tour#metadata) in the +/// language tour. +library meta; + +/// Used to annotate a function `f`. Indicates that `f` always throws an +/// exception. Any functions that override `f`, in class inheritance, are also +/// expected to conform to this contract. +/// +/// Tools, such as the analyzer, can use this to understand whether a block of +/// code "exits". For example: +/// +/// ```dart +/// @alwaysThrows toss() { throw 'Thrown'; } +/// +/// int fn(bool b) { +/// if (b) { +/// return 0; +/// } else { +/// toss(); +/// print("Hello."); +/// } +/// } +/// ``` +/// +/// Without the annotation on `toss`, it would look as though `fn` doesn't +/// always return a value. The annotation shows that `fn` does always exit. In +/// addition, the annotation reveals that any statements following a call to +/// `toss` (like the `print` call) are dead code. +/// +/// Tools, such as the analyzer, can also expect this contract to be enforced; +/// that is, tools may emit warnings if a function with this annotation +/// _doesn't_ always throw. +const _AlwaysThrows alwaysThrows = _AlwaysThrows(); + +/// Used to annotate a parameter of an instance method that overrides another +/// method. +/// +/// Indicates that this parameter may have a tighter type than the parameter on +/// its superclass. The actual argument will be checked at runtime to ensure it +/// is a subtype of the overridden parameter type. +/// +/// DEPRECATED: Use the `covariant` modifier instead. +@deprecated +const _Checked checked = _Checked(); + +/// Used to annotate a library, or any declaration that is part of the public +/// interface of a library (such as top-level members, class members, and +/// function parameters) to indicate that the annotated API is experimental and +/// may be removed or changed at any-time without updating the version of the +/// containing package, despite the fact that it would otherwise be a breaking +/// change. +/// +/// If the annotation is applied to a library then it is equivalent to applying +/// the annotation to all of the top-level members of the library. Applying the +/// annotation to a class does *not* apply the annotation to subclasses, but +/// does apply the annotation to members of the class. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with a declaration that is not part of the +/// public interface of a library (such as a local variable or a declaration +/// that is private) or a directive other than the first directive in the +/// library, or +/// * the declaration is referenced by a package that has not explicitly +/// indicated its intention to use experimental APIs (details TBD). +const _Experimental experimental = _Experimental(); + +/// Used to annotate an instance or static method `m`. Indicates that `m` must +/// either be abstract or must return a newly allocated object or `null`. In +/// addition, every method that either implements or overrides `m` is implicitly +/// annotated with this same annotation. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than a method, or +/// * a method that has this annotation can return anything other than a newly +/// allocated object or `null`. +const _Factory factory = _Factory(); + +/// Used to annotate a class `C`. Indicates that `C` and all subtypes of `C` +/// must be immutable. +/// +/// A class is immutable if all of the instance fields of the class, whether +/// defined directly or inherited, are `final`. +/// +/// Tools, such as the analyzer, can provide feedback if +/// * the annotation is associated with anything other than a class, or +/// * a class that has this annotation or extends, implements or mixes in a +/// class that has this annotation is not immutable. +const Immutable immutable = Immutable(); + +/// Used to annotate a test framework function that runs a single test. +/// +/// Tools, such as IDEs, can show invocations of such function in a file +/// structure view to help the user navigating in large test files. +/// +/// The first parameter of the function must be the description of the test. +const _IsTest isTest = _IsTest(); + +/// Used to annotate a test framework function that runs a group of tests. +/// +/// Tools, such as IDEs, can show invocations of such function in a file +/// structure view to help the user navigating in large test files. +/// +/// The first parameter of the function must be the description of the group. +const _IsTestGroup isTestGroup = _IsTestGroup(); + +/// Used to annotate a const constructor `c`. Indicates that any invocation of +/// the constructor must use the keyword `const` unless one or more of the +/// arguments to the constructor is not a compile-time constant. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than a const constructor, +/// or +/// * an invocation of a constructor that has this annotation is not invoked +/// using the `const` keyword unless one or more of the arguments to the +/// constructor is not a compile-time constant. +const _Literal literal = _Literal(); + +/// Used to annotate an instance method `m`. Indicates that every invocation of +/// a method that overrides `m` must also invoke `m`. In addition, every method +/// that overrides `m` is implicitly annotated with this same annotation. +/// +/// Note that private methods with this annotation cannot be validly overridden +/// outside of the library that defines the annotated method. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than an instance method, +/// or +/// * a method that overrides a method that has this annotation can return +/// without invoking the overridden method. +const _MustCallSuper mustCallSuper = _MustCallSuper(); + +/// Used to annotate an instance member (method, getter, setter, operator, or +/// field) `m` in a class `C` or mixin `M`. Indicates that `m` should not be +/// overridden in any classes that extend or mixin `C` or `M`. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than an instance member, +/// * the annotation is associated with an abstract member (because subclasses +/// are required to override the member), +/// * the annotation is associated with an extension method, +/// * the annotation is associated with a member `m` in class `C`, and there is +/// a class `D` or mixin `M`, that extends or mixes in `C`, that declares an +/// overriding member `m`. +const _NonVirtual nonVirtual = _NonVirtual(); + +/// Used to annotate a class, mixin, or extension declaration `C`. Indicates +/// that any type arguments declared on `C` are to be treated as optional. +/// Tools such as the analyzer and linter can use this information to suppress +/// warnings that would otherwise require type arguments on `C` to be provided. +const _OptionalTypeArgs optionalTypeArgs = _OptionalTypeArgs(); + +/// Used to annotate an instance member (method, getter, setter, operator, or +/// field) `m` in a class `C`. If the annotation is on a field it applies to the +/// getter, and setter if appropriate, that are induced by the field. Indicates +/// that `m` should only be invoked from instance methods of `C` or classes that +/// extend, implement or mix in `C`, either directly or indirectly. Additionally +/// indicates that `m` should only be invoked on `this`, whether explicitly or +/// implicitly. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than an instance member, +/// or +/// * an invocation of a member that has this annotation is used outside of an +/// instance member defined on a class that extends or mixes in (or a mixin +/// constrained to) the class in which the protected member is defined. +/// * an invocation of a member that has this annotation is used within an +/// instance method, but the receiver is something other than `this`. +const _Protected protected = _Protected(); + +/// Used to annotate a named parameter `p` in a method or function `f`. +/// Indicates that every invocation of `f` must include an argument +/// corresponding to `p`, despite the fact that `p` would otherwise be an +/// optional parameter. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than a named parameter, +/// * the annotation is associated with a named parameter in a method `m1` that +/// overrides a method `m0` and `m0` defines a named parameter with the same +/// name that does not have this annotation, or +/// * an invocation of a method or function does not include an argument +/// corresponding to a named parameter that has this annotation. +const Required required = Required(); + +/// Annotation marking a class as not allowed as a super-type. +/// +/// Classes in the same package as the marked class may extend, implement or +/// mix-in the annotated class. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with anything other than a class, +/// * the annotation is associated with a class `C`, and there is a class or +/// mixin `D`, which extends, implements, mixes in, or constrains to `C`, and +/// `C` and `D` are declared in different packages. +const _Sealed sealed = _Sealed(); + +/// Used to annotate a field that is allowed to be overridden in Strong Mode. +/// +/// Deprecated: Most of strong mode is now the default in 2.0, but the notion of +/// virtual fields was dropped, so this annotation no longer has any meaning. +/// Uses of the annotation should be removed. +@deprecated +const _Virtual virtual = _Virtual(); + +/// Used to annotate an instance member that was made public so that it could be +/// overridden but that is not intended to be referenced from outside the +/// defining library. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with a declaration other than a public +/// instance member in a class or mixin, or +/// * the member is referenced outside of the defining library. +const _VisibleForOverriding visibleForOverriding = _VisibleForOverriding(); + +/// Used to annotate a declaration was made public, so that it is more visible +/// than otherwise necessary, to make code testable. +/// +/// Tools, such as the analyzer, can provide feedback if +/// +/// * the annotation is associated with a declaration not in the `lib` folder +/// of a package, or a private declaration, or a declaration in an unnamed +/// static extension, or +/// * the declaration is referenced outside of its the defining library or a +/// library which is in the `test` folder of the defining package. +const _VisibleForTesting visibleForTesting = _VisibleForTesting(); + +/// Used to annotate a class. +/// +/// See [immutable] for more details. +class Immutable { + /// A human-readable explanation of the reason why the class is immutable. + final String reason; + + /// Initialize a newly created instance to have the given [reason]. + const Immutable([this.reason = '']); +} + +/// Used to annotate a named parameter `p` in a method or function `f`. +/// +/// See [required] for more details. +class Required { + /// A human-readable explanation of the reason why the annotated parameter is + /// required. For example, the annotation might look like: + /// + /// ButtonWidget({ + /// Function onHover, + /// @Required('Buttons must do something when pressed') + /// Function onPressed, + /// ... + /// }) ... + final String reason; + + /// Initialize a newly created instance to have the given [reason]. + const Required([this.reason = '']); +} + +class _AlwaysThrows { + const _AlwaysThrows(); +} + +class _Checked { + const _Checked(); +} + +class _Experimental { + const _Experimental(); +} + +class _Factory { + const _Factory(); +} + +class _IsTest { + const _IsTest(); +} + +class _IsTestGroup { + const _IsTestGroup(); +} + +class _Literal { + const _Literal(); +} + +class _MustCallSuper { + const _MustCallSuper(); +} + +class _NonVirtual { + const _NonVirtual(); +} + +class _OptionalTypeArgs { + const _OptionalTypeArgs(); +} + +class _Protected { + const _Protected(); +} + +class _Sealed { + const _Sealed(); +} + +@deprecated +class _Virtual { + const _Virtual(); +} + +class _VisibleForOverriding { + const _VisibleForOverriding(); +} + +class _VisibleForTesting { + const _VisibleForTesting(); +} diff --git a/pkg/linter/test_data/mock_packages/meta/pubspec.yaml b/pkg/linter/test_data/mock_packages/meta/pubspec.yaml new file mode 100644 index 00000000000..c76a3a89840 --- /dev/null +++ b/pkg/linter/test_data/mock_packages/meta/pubspec.yaml @@ -0,0 +1,4 @@ +name: meta +version: 1.1.8 +environment: + sdk: '>=2.2.2 <3.0.0' diff --git a/pkg/linter/test_data/rules/.dart_tool/package_config.json b/pkg/linter/test_data/rules/.dart_tool/package_config.json new file mode 100644 index 00000000000..fb391befcd2 --- /dev/null +++ b/pkg/linter/test_data/rules/.dart_tool/package_config.json @@ -0,0 +1,26 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "fixnum", + "rootUri": "../../mock_packages/fixnum/", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "flutter", + "rootUri": "../../mock_packages/flutter/", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "meta", + "rootUri": "../../mock_packages/meta/", + "packageUri": "lib/", + "languageVersion": "2.17" + } + ], + "generated": "2022-06-30T01:27:31.576781Z", + "generator": "pub", + "generatorVersion": "2.17.0" +} diff --git a/pkg/linter/test_data/rules/always_declare_return_types.dart b/pkg/linter/test_data/rules/always_declare_return_types.dart new file mode 100644 index 00000000000..af474b30998 --- /dev/null +++ b/pkg/linter/test_data/rules/always_declare_return_types.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N always_declare_return_types` + +main() { } //LINT + +bar() => new _Foo(); //LINT + +class _Foo { + _foo() => 42; //LINT +} + +typedef bad(int x); //LINT + +typedef bool predicate(Object o); + +void main2() { } + +_Foo bar2() => new _Foo(); + +class _Foo2 { + int _foo() => 42; +} + +set speed(int ms) {} //OK + +class Car { + static set make(String name) {} // OK + set speed(int ms) {} //OK +} + +abstract class MyList implements List { + @override + operator []=(int index, E value) //OK: #300 + { + // ignored. + } +} + +class A { } + +extension Foo on A { + foo() { } // LINT +} diff --git a/pkg/linter/test_data/rules/always_put_control_body_on_new_line.dart b/pkg/linter/test_data/rules/always_put_control_body_on_new_line.dart new file mode 100644 index 00000000000..39ecb22dfba --- /dev/null +++ b/pkg/linter/test_data/rules/always_put_control_body_on_new_line.dart @@ -0,0 +1,87 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N always_put_control_body_on_new_line` + +testIfElse() { + if (false) return; // LINT + + if (false) {} // OK + + if (false) + return; // OK + + if (false) { // OK + } + + if (false) + return; // OK + else return; // LINT + + if (false) { + } + else if (false) { // OK + } + else { + } + + if (false) { // OK + } else if (false) { // OK + } else { // OK + } + + if (false) { } // OK + else return; // LINT + + if (false) + return; // OK + else + return; // OK + + if (false){ }// OK + else {} // OK + + if (false) print( // LINT + 'First argument' + 'Second argument'); + + if (false) { print('should be on next line'); // LINT + } +} + +testWhile() { + while (true) return; // LINT + + while (true) {} // OK + + while (true) + return; // OK +} + +testForEach(List l) { + for (var i in l) return; // LINT + + for (var i in l) {} // OK + + for (var i in l) + return; // OK +} + +testFor() { + for (;;) return; // LINT + + for (;;) {} // OK + + for (;;) + return; // OK +} + +testDo() { + do print(''); // LINT + while (true); + + do + print(''); // OK + while (true); +} diff --git a/pkg/linter/test_data/rules/always_put_required_named_parameters_first.dart b/pkg/linter/test_data/rules/always_put_required_named_parameters_first.dart new file mode 100644 index 00000000000..9c169f85738 --- /dev/null +++ b/pkg/linter/test_data/rules/always_put_required_named_parameters_first.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N always_put_required_named_parameters_first` + +import 'package:meta/meta.dart'; + +m1( + a, // OK + { + b, // OK + @required c, // LINT + @required d, // LINT + e, // OK + @required f, // LINT +}) {} + +m2({ + @required a, // OK + @required b, // OK + c, // OK + @required d, // LINT + e, // OK + @required f, // LINT +}) {} + +n1( + a, // OK + { + b, // OK + required c, // LINT + required d, // LINT + e, // OK + required f, // LINT +}) {} + +n2({ + required a, // OK + required b, // OK + c, // OK + required d, // LINT + e, // OK + required f, // LINT +}) {} + +class A { + A.c1( + a, // OK + { + b, // OK + @required c, // LINT + @required d, // LINT + e, // OK + @required f, // LINT + }); + A.c2({ + @required a, // OK + @required b, // OK + c, // OK + @required d, // LINT + e, // OK + @required f, // LINT + }); + A.d1( + a, // OK + { + b, // OK + required c, // LINT + required d, // LINT + e, // OK + required f, // LINT + }); + A.d2({ + required a, // OK + required b, // OK + c, // OK + required d, // LINT + e, // OK + required f, // LINT + }); +} diff --git a/pkg/linter/test_data/rules/always_require_non_null_named_parameters.dart b/pkg/linter/test_data/rules/always_require_non_null_named_parameters.dart new file mode 100644 index 00000000000..d1073ff2dec --- /dev/null +++ b/pkg/linter/test_data/rules/always_require_non_null_named_parameters.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N always_require_non_null_named_parameters` + +// @dart=2.9 + +import 'package:meta/meta.dart'; + +var condition = true; + +m0({ + Object a, // OK +}) { + if (condition) { + return; + } + assert(a != null); +} + +m1( + a, + b, + c, { + d, // LINT + e: '', + @required f, // OK + g, // LINT + h: 1, // OK +}) { + assert(null != d); + assert(g != null); + assert(f != null); + assert(h != null); +} + +class A { + A({ + @required a, // OK + b, // LINT + @required c, // OK + }) { + assert(a != null); + assert(b != null); + } + + m1({ + @required a, // OK + b, // LINT + @required c, // OK + }) { + assert(a != null); + assert(b != null); + } + + m2({ + @required a, // OK + b, // LINT + @required c, // OK + }) { + assert(true && a != null); + assert(b != null && true); + } +} diff --git a/pkg/linter/test_data/rules/always_specify_types.dart b/pkg/linter/test_data/rules/always_specify_types.dart new file mode 100644 index 00000000000..8668aaa6f11 --- /dev/null +++ b/pkg/linter/test_data/rules/always_specify_types.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N always_specify_types` + +// ignore_for_file: unused_local_variable + +import 'package:meta/meta.dart'; + +/// https://github.com/dart-lang/linter/issues/3275 +typedef Foo1 = Map; +final Foo1 foo = Foo1(); + +/// Constructor tear-offs +void constructorTearOffs() { + List.filled; // LINT + List Function(int, E) filledList = List.filled; // OK - generic function + filledList(3, 3); // OK + filledList(3, 3); // OK - generic function invocations are uncovered -- see: #2914 +} + +typedef MapList = List; //LINT +typedef JsonMap = Map; //OK +typedef StringList = List; //OK +typedef RawList = List; //LINT +typedef StringMap = Map; //OK + +StringMap sm = StringMap(); //OK +StringMap? rm1; //LINT +StringMap rm2 = StringMap(); //LINT +StringMap rm3 = StringMap(); //LINT + +Map map = {}; //LINT +List strings = []; //LINT +Set set = {}; //LINT + +List? list; //LINT +List? lists; //LINT +List ints = [1]; //OK + +final x = 1; //LINT [1:5] Missing type annotation. +final x1 = 1, x2 = '', x3 = 1.2; //LINT [1:5] +final int xx = 3; +const y = 2; //LINT Missing type annotation. +const int yy = 3; +String? s1 = ''; +var s2 = '', s3 = s1; //LINT Missing type annotation. + +a(var x) {} //LINT Missing type annotation. +b(s) {} //LINT [3:1] Missing type annotation. +c(int x) {} +d(final x) {} //LINT +e(final int x) {} + +@optionalTypeArgs +class P { } + +@optionalTypeArgs +void g() {} + +//https://github.com/dart-lang/linter/issues/851 +void test() { + g(); //OK + g(); //OK +} + +main() { + var x = ''; //LINT [3:3] Missing type annotation. + var x1 = '', x2 = 1.2; //LINT [3:3] + for (var i = 0; i < 10; ++i) { //LINT [8:3] Missing type annotation. + print(i); + } + List ls = []; + // ignore: avoid_function_literals_in_foreach_calls + ls.forEach((s) => print(s)); //LINT [15:1] Missing type annotation. + ls.forEach((var s) => print(s)); //LINT [15:3] Missing type annotation. + for (var l in ls) { //LINT [8:3] Missing type annotation. + print(l); + } + try { + for (final l in ls) { // LINT [10:5] Missing type annotation. + print(l); + } + } on Exception catch (ex) { + print(ex); + } catch (e) { // NO warning (https://codereview.chromium.org/1427223002/) + print(e); + } + + // ignore: non_constant_identifier_names + var __; // LINT + + listen((_) { // OK! + // ... + }); + + P p = P(); //OK (optionalTypeArgs) +} + +P doSomething(P p) //OK (optionalTypeArgs) +{ + return p; +} + +listen(void Function(Object event) onData) {} + +var z; //LINT + +class Foo { + static var bar; //LINT + static final baz = 1; //LINT Missing type annotation. + static final int bazz = 42; + var foo; //LINT Missing type annotation. + Foo(var bar); //LINT [7:3] Missing type annotation. + void f(List l) { } //LINT +} + +void m() { + if ('' is Map) //OK + { + print("won't happen"); + } +} diff --git a/pkg/linter/test_data/rules/avoid_bool_literals_in_conditional_expressions.dart b/pkg/linter/test_data/rules/avoid_bool_literals_in_conditional_expressions.dart new file mode 100644 index 00000000000..f03862f3086 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_bool_literals_in_conditional_expressions.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_bool_literals_in_conditional_expressions` + +late bool a, b, c; + +var d = a ? true : b; // LINT +var e = a ? false : b; // LINT +var f = a ? b : true; // LINT +var g = a ? b : false; // LINT +var h = a ? b : c; // OK diff --git a/pkg/linter/test_data/rules/avoid_catches_without_on_clauses.dart b/pkg/linter/test_data/rules/avoid_catches_without_on_clauses.dart new file mode 100644 index 00000000000..373c7dabdfc --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_catches_without_on_clauses.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_catches_without_on_clauses` + +void bad() { + try {} + catch (e) { // LINT + // ignore + } +} + +void good() { + try {} + on Exception catch (e) { // OK + // ignore + } +} diff --git a/pkg/linter/test_data/rules/avoid_catching_errors.dart b/pkg/linter/test_data/rules/avoid_catching_errors.dart new file mode 100644 index 00000000000..6d6ab870cfb --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_catching_errors.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_catching_errors` + +// ignore_for_file: unused_catch_clause + +void bad() { + try {} on A catch (e) { // LINT + // ignore + } on B catch (e) { // LINT + // ignore + } on C { // LINT + // ignore + } on D { // LINT + // ignore + } on Error catch (e) { // LINT + // ignore + } on Exception catch (e) { // OK + // ignore + } on String catch (e) { // OK + // ignore + } catch (e) {// OK + // ignore + } +} + +class A extends B {} + +class B implements Error { + @override + StackTrace? get stackTrace => null; +} + +class C extends D {} + +class D extends Error {} diff --git a/pkg/linter/test_data/rules/avoid_classes_with_only_static_members.dart b/pkg/linter/test_data/rules/avoid_classes_with_only_static_members.dart new file mode 100644 index 00000000000..8f5d1eed44e --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_classes_with_only_static_members.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_classes_with_only_static_members` + +class Bad // LINT +{ + static int a = 0; + + static foo() {} +} + +class Ok extends Good1 // OK +{ + static int staticInt = 0; + + static foo() {} +} + +class Bad3 // OK +{} + +class Good1 // OK +{ + int a = 0; +} + +class Good2 // OK +{ + void foo() {} +} + +class Good3 // OK +{ + Good3(); +} + +class Color // OK +{ + static const red = '#f00'; + static const green = '#0f0'; + static const blue = '#00f'; + static const black = '#000'; + static const white = '#fff'; +} + +class DateUtils // LINT +{ + static DateTime mostRecent(List dates) => dates.first; +} + +class F // OK +{ + F._(); + static int f = 0; +} diff --git a/pkg/linter/test_data/rules/avoid_double_and_int_checks.dart b/pkg/linter/test_data/rules/avoid_double_and_int_checks.dart new file mode 100644 index 00000000000..135db6d7545 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_double_and_int_checks.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_double_and_int_checks` + +lint_for_double_before_int_check_on_parameter(m) { + if (m is double) {} // + else if (m is int) {} // LINT +} + +no_lint_for_only_double_check(m) { + if (m is double) {} // OK +} + +no_lint_for_int_before_double_check(m) { + if (m is int) {} // + else if (m is double) {} // OK +} + +lint_for_double_before_int_check_on_local() { + var m; + if (m is double) {} // + else if (m is int) {} // LINT +} + +get g => null; +lint_for_double_before_int_check_on_getter() { + if (g is double) {} // + else if (g is int) {} // OK +} diff --git a/pkg/linter/test_data/rules/avoid_dynamic_calls.dart b/pkg/linter/test_data/rules/avoid_dynamic_calls.dart new file mode 100644 index 00000000000..94b624c5f69 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_dynamic_calls.dart @@ -0,0 +1,192 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test test/rule_test.dart -N avoid_dynamic_calls` + +import 'dart:core'; +import 'dart:core' as core_top_level_prefix_as; + +void explicitDynamicType(dynamic object) { + object.foo(); // LINT + object.bar; // LINT +} + +void implicitDynamicType(object) { + object.foo(); // LINT + object.bar; // LINT +} + +// This would likely not pass at runtime, but we're using it for inference only. +T genericType() => null as T; + +void inferredDynamicType() { + var object = genericType(); + object.foo(); // LINT + object.bar; // LINT +} + +class Wrapper { + final T field; + Wrapper(this.field); +} + +void fieldDynamicType(Wrapper wrapper) { + wrapper.field.foo(); // LINT + wrapper.field.bar; // LINT + wrapper.field(); // LINT + (wrapper.field)(); // LINT +} + +void cascadeExpressions(dynamic a, Wrapper b) { + a..b; // LINT + b..field; // OK + b + ..toString + ..field.a() // LINT + ..field.b; // LINT +} + +class TearOffFunction { + static int staticDoThing() => 0; + + int doThing() => 0; +} + +void otherPropertyAccessOrCalls(dynamic a) { + a(); // LINT + a?.b; // LINT + a!.b; // LINT + a?.b(); // LINT + a!.b(); // LINT + (a).b; // LINT +} + +void tearOffFunctions(TearOffFunction a) { + var p = core_top_level_prefix_as.print; // OK + core_top_level_prefix_as.print('Hello'); // OK + p('Hello'); // OK + var doThing = a.doThing; // OK + doThing(); // OK + var staticDoThing = TearOffFunction.staticDoThing; // OK + staticDoThing(); // OK + identical(true, false); // OK + var alsoIdentical = identical; + alsoIdentical(true, false); // OK +} + +typedef F = void Function(); + +void functionExpressionInvocations( + dynamic a(), + Function b(), + void Function() c, + d(), + F f, + F? fn, +) { + a(); // OK + a()(); // LINT + b(); // OK + b()(); // LINT + c(); // OK + d(); // OK + f.call; // OK + f.call(); // OK + fn?.call; // OK + fn?.call(); // OK +} + +void typedFunctionButBasicallyDynamic(Function a, Wrapper b) { + a(); // LINT + b.field(); // LINT + (b.field)(); // LINT + a.call; // OK + a.call(); // LINT +} + +void binaryExpressions(dynamic a, int b, bool c) { + a + a; // LINT + a + b; // LINT + a > b; // LINT + a < b; // LINT + a >= b; // LINT + a <= b; // LINT + a ^ b; // LINT + a | b; // LINT + a & b; // LINT + a % b; // LINT + a / b; // LINT + a ~/ b; // LINT + a >> b; // LINT + a << b; // LINT + a || c; // OK; this is an implicit downcast, not a dynamic call + a && c; // OK; this is an implicit downcast, not a dynamic call + b + a; // OK; this is an implicit downcast, not a dynamic call + a ?? b; // OK; this is a null comparison, not a dynamic call. + a is int; // OK + a is! int; // OK + a as int; // OK +} + +void equalityExpressions(dynamic a, dynamic b) { + a == b; // OK, see lint description for details. + a == null; // OK. + a != b; // OK + a != null; // OK. +} + +void membersThatExistOnObject(dynamic a, Invocation b) { + a.hashCode; // OK + a.runtimeType; // OK + a.noSuchMethod(); // LINT + a.noSuchMethod(b); // OK + a.noSuchMethod(b, 1); // LINT + a.noSuchMethod(b, name: 1); // LINT + a.toString(); // OK + a.toString(1); // LINT + a.toString(name: 1); // LINT + '$a'; // OK + '${a}'; // OK +} + +void memberTearOffsOnObject(dynamic a, Invocation b) { + var tearOffNoSuchMethod = a.noSuchMethod; // OK + tearOffNoSuchMethod(b); // OK + var tearOffToString = a.toString; // OK + tearOffToString(); // OK +} + +void assignmentExpressions(dynamic a) { + a += 1; // LINT + a -= 1; // LINT + a *= 1; // LINT + a ^= 1; // LINT + a /= 1; // LINT + a &= 1; // LINT + a |= 1; // LINT + a ??= 1; // OK +} + +void prefixExpressions(dynamic a, int b) { + !a; // LINT + -a; // LINT + ++a; // LINT + --a; // LINT + ++b; // OK + --b; // OK +} + +void postfixExpressions(dynamic a, int b) { + a!; // OK; this is not a dynamic call. + a++; // LINT + a--; // LINT + b++; // OK + b--; // OK +} + +void indexExpressions(dynamic a) { + a[1]; // LINT + a[1] = 1; // LINT + a = a[1]; // LINT +} diff --git a/pkg/linter/test_data/rules/avoid_empty_else.dart b/pkg/linter/test_data/rules/avoid_empty_else.dart new file mode 100644 index 00000000000..6695157fbe2 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_empty_else.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_empty_else` + +f() { + int x = 1, y = 0; + if (x > y) + print("1"); + else ; //LINT + print("2"); +} + +g() { + int x = 1, y = 0; + if (x > y) + print("1"); + else + print("2"); +} diff --git a/pkg/linter/test_data/rules/avoid_field_initializers_in_const_classes.dart b/pkg/linter/test_data/rules/avoid_field_initializers_in_const_classes.dart new file mode 100644 index 00000000000..c5de0ff2d6a --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_field_initializers_in_const_classes.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_field_initializers_in_const_classes` + +class A { + final a = const []; // LINT + const A(); +} + +class B { + final a; + const B() // + : a = const []; // LINT +} + +class C { + final a; + const C(this.a); // OK +} + +class D { + final a; + const D(b) // + : a = b; // OK +} + +// no lint if several constructors +class E { + final a; + const E.c1() // + : a = const []; // OK + const E.c2() // + : a = const {}; // OK +} + +class F { + final a; + const F(int a) : this.a = 0; // LINT +} + +class G { + final g; + const G(int length) : g = 'xyzzy'.length; // LINT +} + +mixin M { + final a = const []; // OK +} diff --git a/pkg/linter/test_data/rules/avoid_implementing_value_types.dart b/pkg/linter/test_data/rules/avoid_implementing_value_types.dart new file mode 100644 index 00000000000..f229a621086 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_implementing_value_types.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_implementing_value_types` + +class Size { // OK + final int inBytes; + const Size(this.inBytes); + + @override + bool operator ==(Object o) => o is Size && o.inBytes == inBytes; + + @override + int get hashCode => inBytes.hashCode; +} + +class SizeWithKilobytes extends Size { // OK + SizeWithKilobytes(int inBytes) : super(inBytes); + + double get inKilobytes => inBytes / 1000; +} + +class EmptyFileSize1 implements Size { // LINT + @override + int get inBytes => 0; +} + +class EmptyFileSize2 implements SizeWithKilobytes { // LINT + @override + int get inBytes => 0; + + @override + double get inKilobytes => 0.0; +} + +mixin SizeClassMixin { // OK + int get inBytes => 0; + + @override + bool operator ==(Object o) => o is Size && o.inBytes == o.inBytes; + + @override + int get hashCode => inBytes.hashCode; +} + +class UsesSizeClassMixin extends Object with SizeClassMixin {} // OK diff --git a/pkg/linter/test_data/rules/avoid_init_to_null.dart b/pkg/linter/test_data/rules/avoid_init_to_null.dart new file mode 100644 index 00000000000..1d753cf7d4d --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_init_to_null.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// @dart=2.9 + +// test w/ `dart test -N avoid_init_to_null` + +var x = null; //LINT +var y; //OK +var z = 1; //OK +const nil = null; //OK +final nil2 = null; //OK +foo({p: null}) {} //LINT + +class X { + static const nil = null; //OK + final nil2 = null; //OK + int x = null; //LINT + int y; //OK + int z; + + X({int a: null}); //LINT + X.b({this.z: null}); //LINT + + fooNamed({ + p: null, //LINT + p1 = null, //LINT + var p2 = null, //LINT + p3 = 1, //OK + p4, //OK + }) {} + + fooOptional([ + p = null, //LINT + p1 = null, //LINT + var p2 = null, //LINT + p3 = 1, //OK + p4, //OK + ]) {} +} diff --git a/pkg/linter/test_data/rules/avoid_js_rounded_ints.dart b/pkg/linter/test_data/rules/avoid_js_rounded_ints.dart new file mode 100644 index 00000000000..32f0522f8e2 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_js_rounded_ints.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_js_rounded_ints` + +final i1 = 1; // OK +final i2 = -45321; // OK +final i3 = 24361; // OK +final i4 = 245452; // OK +final min = -9007199254740991; // OK +final max = 9007199254740991; // OK + +final minErr = -9007199254740993; // LINT +final maxErr = 9007199254740993; // LINT + +final notRounded = 1000000000000000000; // OK +final rounded = 1000000000000000001; // LINT + +// value.abs() for this number is negative on the 64-bit integer VM. +// Lucky it is not rounded! (-2^63) +final absNegative = -9223372036854775808; // OK diff --git a/pkg/linter/test_data/rules/avoid_multiple_declarations_per_line.dart b/pkg/linter/test_data/rules/avoid_multiple_declarations_per_line.dart new file mode 100644 index 00000000000..d35c9a40daa --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_multiple_declarations_per_line.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_multiple_declarations_per_line` + +// ignore_for_file: unused_local_variable + +String? badFoo, badBar, badBaz; // LINT + +String? goodFoo; +String? goodBar; +String? goodBaz; + +methodContainingBadDeclaration() { + String? badFoo, badBar, badBaz; // LINT +} + +methodContainingGoodDeclaration() { + String? goodFoo; + String? goodBar; + String? goodBaz; +} + +class BadClass { + String? foo, bar, baz; // LINT + + methodContainingBadDeclaration() { + String? badFoo, badBar, badBaz; // LINT + } +} + +class GoodClass { + String? foo; + String? bar; + String? baz; + + methodContainingGoodDeclaration() { + String? goodFoo; + String? goodBar; + String? goodBaz; + } +} + +extension BadExtension on Object { + static String? badFoo, badBar, badBaz; // LINT +} + +extension GoodExtension on Object { + static String? foo; + static String? bar; + static String? baz; +} + +// https://github.com/dart-lang/linter/issues/2543 +okInForLoop() { + for (var i = 0, j = 0; i < 2 && j < 2; ++i, ++j) // OK + { + // + } +} diff --git a/pkg/linter/test_data/rules/avoid_null_checks_in_equality_operators.dart b/pkg/linter/test_data/rules/avoid_null_checks_in_equality_operators.dart new file mode 100644 index 00000000000..019a3b77ad4 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_null_checks_in_equality_operators.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_null_checks_in_equality_operators` + + +class P { + final String name = ''; + + @override + operator ==(other) { + return other != null && //OK: https://github.com/dart-lang/linter/issues/2864 + other is P && + name == other.name; + } +} + +class P2 { + final String name = ''; + + @override + operator ==(Object other) { + return other != null && //OK: https://github.com/dart-lang/linter/issues/2864 + other is P2 && + name == other.name; + } +} + +class BadPerson1 { + final String name = 'I am a bad person'; + + get age => 42; + + @override + operator ==(Object? other) => + other != null && // LINT + other is BadPerson1 && + name == other.name; +} + +class BadPerson2 { + final String name = 'I am a bad person'; + + @override + operator ==(Object? other) => + !(other == null) && // LINT + other is BadPerson2 && + name == other.name; +} + +class BadPerson3 { + final String name = 'I am a bad person'; + + @override + operator ==(Object? other) => + other is BadPerson3 + && + name == other?.name; // LINT +} + +class BadPerson4 { + final String name = 'I am a bad person'; + + String getName() => name; + + @override + operator ==(Object? other) => + other is BadPerson4 + && + name == other?.getName(); // LINT +} + +class BadPerson5 { + String name; + + BadPerson5(this.name); + + @override + operator ==(Object? other) { + if (other is BadPerson5){ + final toCompare = other ?? new BadPerson5(""); // LINT + return toCompare.name == name; + } + return false; + } + +} + +class GoodPerson { + final String name = 'I am a good person'; + + @override + operator ==(other) => other is GoodPerson && name == other.name; // OK +} diff --git a/pkg/linter/test_data/rules/avoid_print.dart b/pkg/linter/test_data/rules/avoid_print.dart new file mode 100644 index 00000000000..492b0bb8334 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_print.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_print` + +import 'package:flutter/foundation.dart'; + +void main() { + print('ha'); // LINT + [1,2,3].forEach(print); // LINT + Future.value('hello').then(print); // LINT + if (kDebugMode) print(''); // OK + if (kDebugMode) { + print(''); // OK + } +} + + +var x = print; // OK + +void f() { + x('ha'); // OK? + [1,2,3].forEach(x); // OK + Future.value('hello').then(x); // OK +} + +class A { + print() { + } +} + +void g() { + A().print(); // OK +} diff --git a/pkg/linter/test_data/rules/avoid_redundant_argument_values.dart b/pkg/linter/test_data/rules/avoid_redundant_argument_values.dart new file mode 100644 index 00000000000..8825e05d8b0 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_redundant_argument_values.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_redundant_argument_values` + +import 'package:meta/meta.dart'; + +class A { + A({bool valWithDefault = true, bool? val}); + void f({bool valWithDefault = true, bool? val}) {} + void g({int valWithDefault = 1, bool? val}) {} + void h({String valWithDefault = 'default', bool? val}) {} +} + +enum TestEnum { + a(test: false); // LINT + + const TestEnum({this.test = false}); + + final bool test; +} + +f(void g([bool? b])) { + // Function Expression Invocation. + g(null); // TODO: https://github.com/dart-lang/linter/issues/4368 +} + +bool q() => true; + +void ff({bool valWithDefault = true, bool? val}) {} +void g({@required bool valWithDefault = true, bool? val}) {} + +void gg(int x, [int y = 0]) {} +void ggg([int a = 1, int b = 2]) {} +void gggg([int a = 0, int? b]) {} + +void h([int? a, int? b = 1]) {} + +void main() { + + // Tear-off + var aCons = A.new; + aCons(valWithDefault: true); //LINT + + A(valWithDefault: true); //LINT + A().f(valWithDefault: true); //LINT + A().g(valWithDefault: 1); //LINT + A().h(valWithDefault: 'default'); //LINT + + A().f(val: false); //OK + A().f(val: false, valWithDefault: false); //OK + + final v = true; + A().f(val: false, valWithDefault: v); //OK + A().f(val: false, valWithDefault: q()); //OK + + ff(valWithDefault: true); //LINT + ff(val: false); //OK + ff(val: false, valWithDefault: false); //OK + + ff(val: false, valWithDefault: v); //OK + ff(val: false, valWithDefault: q()); //OK + + void fff({bool valWithDefault = true, bool? val}) {} + + fff(valWithDefault: true); //LINT + fff(val: false); //OK + fff(val: false, valWithDefault: false); //OK + + fff(val: false, valWithDefault: v); //OK + fff(val: false, valWithDefault: q()); //OK + + // Required. + g(valWithDefault: true); //OK + + // Optional positional. + gg(1, 0); //LINT + gg(1, 1); //OK + gg(1); //OK + + ggg( + 1, // OK the - first argument is required so that we can provide the second argument. + 3); + ggg(1, + 2); // LINT + + gggg(0, 1); //OK + + h(0, + 1); //LINT +} diff --git a/pkg/linter/test_data/rules/avoid_returning_null.dart b/pkg/linter/test_data/rules/avoid_returning_null.dart new file mode 100644 index 00000000000..a14783c40d4 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_returning_null.dart @@ -0,0 +1,231 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_returning_null` + +// @dart=2.9 + +bool check = true; + +bool getBool1() => null; // LINT +num getNum1() => null; // LINT +int getInt1() => null; // LINT +double getDouble1() => null; // LINT + +bool getBool2() { + if (check) { + return null; // LINT + } + return true; +} + +num getNum2() { + if (check) { + return null; // LINT + } + return 0; +} + +int getInt2() { + if (check) { + return null; // LINT + } + return 0; +} + +double getDouble2() { + if (check) { + return null; // LINT + } + return 0.0; +} + +class Bad1 { + bool getBool1() => null; // LINT + num getNum1() => null; // LINT + int getInt1() => null; // LINT + double getDouble1() => null; // LINT + + bool getBool2() { + if (check) { + return null; // LINT + } + return true; + } + + num getNum2() { + if (check) { + return null; // LINT + } + return 0; + } + + int getInt2() { + if (check) { + return null; // LINT + } + return 0; + } + + double getDouble2() { + if (check) { + return null; // LINT + } + return 0.0; + } +} + +class Bad2 { + static bool getBool1() => null; // LINT + static num getNum1() => null; // LINT + static int getInt1() => null; // LINT + static double getDouble1() => null; // LINT + + static bool getBool2() { + if (check) { + return null; // LINT + } + return true; + } + + static num getNum2() { + if (check) { + return null; // LINT + } + return 0; + } + + static int getInt2() { + if (check) { + return null; // LINT + } + return 0; + } + + static double getDouble2() { + if (check) { + return null; // LINT + } + return 0.0; + } +} + +class Bad3 { + bool get bool1 => null; // LINT + num get num1 => null; // LINT + int get int1 => null; // LINT + double get double1 => null; // LINT + + bool get bool2 { + if (check) { + return null; // LINT + } + return true; + } + + num get num2 { + if (check) { + return null; // LINT + } + return 0; + } + + int get int2 { + if (check) { + return null; // LINT + } + return 0; + } + + double get double2 { + if (check) { + return null; // LINT + } + return 0.0; + } +} + +class Bad4 { + static bool get bool1 => null; // LINT + static num get num1 => null; // LINT + static int get int1 => null; // LINT + static double get double1 => null; // LINT + + static bool get bool2 { + if (check) { + return null; // LINT + } + return true; + } + + static num get num2 { + if (check) { + return null; // LINT + } + return 0; + } + + static int get int2 { + if (check) { + return null; // LINT + } + return 0; + } + + static double get double2 { + if (check) { + return null; // LINT + } + return 0.0; + } +} + +class A { + dynamic foo() { + return null; // OK + } + + A bar() { + return null; // OK + } +} + +// Exclude local function expressions. +class B { + bool getBool2() { + final foo = () { + return null; // OK + }; + + foo(); + return true; + } + + num getNum2() { + final foo = () { + return null; // OK + }; + + foo(); + return 0; + } + + int getInt2() { + final foo = () { + return null; // OK + }; + + foo(); + return 0; + } + + double getDouble2() { + final foo = () { + return null; // OK + }; + + foo(); + return 0.0; + } +} diff --git a/pkg/linter/test_data/rules/avoid_returning_null_for_future.dart b/pkg/linter/test_data/rules/avoid_returning_null_for_future.dart new file mode 100644 index 00000000000..f106e28ba8b --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_returning_null_for_future.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_returning_null_for_future` + +// @dart=2.8 + +import 'dart:async'; + +Future f1() => null; // LINT +Future f2() { + return null; // LINT +} + +Future f3() async => null; // OK +Future f4() async { + return null; // OK +} + +int f5() => null; // OK +int f6() { + return null; // OK +} + +class C1 { + Future f1() => null; // LINT + Future f2() { + return null; // LINT + } + + Future f3() async => null; // OK + Future f4() async { + return null; // OK + } + + int f5() => null; // OK + int f6() { + return null; // OK + } +} diff --git a/pkg/linter/test_data/rules/avoid_returning_null_for_void.dart b/pkg/linter/test_data/rules/avoid_returning_null_for_void.dart new file mode 100644 index 00000000000..b802f1aed38 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_returning_null_for_void.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_returning_null_for_void` + +import 'dart:async'; + +void f1() { + return null; // LINT +} + +void f2() => null; //LINT + +Future f3() async { + return null; // LINT +} + +Future f4() async => null; // LINT + +class A { + void m1() { + return null; // LINT + } + + void m2() => null; //LINT + + Future m3() async { + return null; // LINT + } + + Future m4() async => null; // LINT +} + +local_functions() { + void f1() { + return null; // LINT + } + + void f2() => null; //LINT + + Future f3() async { + return null; // LINT + } + + Future f4() async => null; // LINT +} diff --git a/pkg/linter/test_data/rules/avoid_returning_this.dart b/pkg/linter/test_data/rules/avoid_returning_this.dart new file mode 100644 index 00000000000..258d770912b --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_returning_this.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_returning_this` + +// ignore_for_file: unused_element + +class A { + int x = 0; + A badAddOne() { + x++; + return this; // LINT + } + + Object goodAddOne1() { // OK it is ok because it does not return an A type. + x++; + return this; + } + + int goodAddOne2() { // OK + x++; + return this.x; + } + A getThing() { + return this; // LINT + } + + B doSomething() { // OK it is ok because it does not return an A type. + x++; + return B(); + } + + A operator +(int n) { // OK it is ok because it is an operator. + x += n; + return this; + } +} + +class B extends A{ + @override + A badAddOne() { // OK it is ok because it is an inherited method. + x++; + return this; + } + + @override + B doSomething() { // OK it is ok because it is an inherited method. + x++; + return this; + } + + B badAddOne2() { + x++; + return this; // LINT + } + + B badOne3() { + int a() { + return 1; + } + x = a(); + return this; // LINT + } + + B badOne4() { + int a() => 1; + x = a(); + return this; // LINT + } + + B badOne5() { + final a = () { + return 1; + }; + x = a(); + return this; // LINT + } +} + +abstract class C { + T m(); + T get g; +} + +class D implements C { + @override + D m() { // OK defined in interface + return this; + } + + @override + D get g { // OK defined in interface + return this; + } + + D _m() { + return this; // LINT + } +} +class E implements C { + @override + E m() => this; // OK defined in interface + + @override + E get g => this; // OK defined in interface + + E _m() => this; // LINT +} + +extension Ext on A { + A ext() => this; // OK +} diff --git a/pkg/linter/test_data/rules/avoid_setters_without_getters.dart b/pkg/linter/test_data/rules/avoid_setters_without_getters.dart new file mode 100644 index 00000000000..eb789fa42fe --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_setters_without_getters.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_setters_without_getters` + +class A { + set x(int x) {} // LINT +} + +class B extends A { + @override + set x(int x) {} // OK because it is an inherited setter. +} + +class C { + int get x => 0; +} + +class D extends C { + set x(int x) {} // OK because has inherited getter. +} + +class E { + int get length => 0; + + set length (int v) {} // OK +} diff --git a/pkg/linter/test_data/rules/avoid_shadowing_type_parameters.dart b/pkg/linter/test_data/rules/avoid_shadowing_type_parameters.dart new file mode 100644 index 00000000000..94d809ffb75 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_shadowing_type_parameters.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_shadowing_type_parameters` +void fn1() { + void fn2() {} // LINT + void fn3() {} // OK + void fn4() {} // OK +} + +// TODO(srawlins): Lint on this stuff as well when the analyzer/language(?) +// support it. Right now analyzer spits out a compile time error: "Analysis of +// generic function typed parameters is not yet supported." +// void fn2(void Function()) {} // NOT OK + +class A { + static void fn1() {} // OK +} + +extension Ext on A { + void fn2() {} // LINT + void fn3() {} // OK + void fn4() {} // OK +} + +mixin M { + void fn1() {} // LINT + void fn2() {} // OK + void fn3() {} // OK +} + +class B { + void fn1() {} // LINT + void fn2() {} // OK + void fn3() {} // OK +} + +class C { + void fn1() { + void fn2() {} // LINT + void fn3() {} // LINT + void fn4() {} // OK + void fn5() {} // OK + } +} + +class D { + void fn1() { + void fn2() { + void fn3() {} // LINT + void fn4() {} // LINT + void fn5() {} // LINT + void fn6() {} // OK + void fn7() {} // OK + } + } +} + +// Make sure we don't hit any null pointers when none of a function or method's +// ancestors have type parameters. +class E { + void fn1() { + void fn2() { + void fn3() {} // OK + } + } + + void fn4() {} // OK +} + +typedef Fn1 = void Function(T); // LINT +typedef Fn2 = void Function(T); // OK +typedef Fn3 = void Function(U); // OK +typedef Fn4 = void Function(T); // OK +typedef Fn5 = void Function(T); // OK + diff --git a/pkg/linter/test_data/rules/avoid_slow_async_io.dart b/pkg/linter/test_data/rules/avoid_slow_async_io.dart new file mode 100644 index 00000000000..d0edf984dab --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_slow_async_io.dart @@ -0,0 +1,111 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_slow_async_io` + +import 'dart:async'; +import 'dart:io'; +import 'dart:io' as io; + +Future some_File_lastModified() async { + var file = new File('/path/to/my/file'); + var now = new DateTime.now(); + if ((await file.lastModified()).isBefore(now)) print('before'); // LINT +} + +Future some_File_lastModifiedSync() async { + var file = new File('/path/to/my/file'); + var now = new DateTime.now(); + if (file.lastModifiedSync().isBefore(now)) print('before'); // OK +} + +Future some_File_exists() async { + var file = new File('/path/to/my/file'); + if (await file.exists()) print('exists'); // LINT +} + +Future some_File_existsSync() async { + var file = new File('/path/to/my/file'); + if (file.existsSync()) print('before'); // OK +} + +Future some_File_stat() async { + var file = new File('/path/to/my/file'); + if (await file.stat() == null) print('stat'); // LINT +} + +Future some_File_statSync() async { + var file = new File('/path/to/my/file'); + if (file.statSync() == null) print('stat'); // OK +} + +Future some_FileSystemEntity_isDirectory() async { + String path = '/path/to/my/file/entity'; + if (await FileSystemEntity.isDirectory(path)) print('dir'); // LINT +} + +Future some_FileSystemEntity_isDirectorySync() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.isDirectorySync(path)) print('dir'); // OK +} + +Future some_FileSystemEntity_isFile() async { + String path = '/path/to/my/file/entity'; + if (await io.FileSystemEntity.isFile(path)) print('file'); // LINT +} + +Future some_FileSystemEntity_isFileSync() async { + String path = '/path/to/my/file/entity'; + if (io.FileSystemEntity.isFileSync(path)) print('file'); // OK +} + +Future some_FileSystemEntity_isLink() async { + String path = '/path/to/my/file/entity'; + if (await FileSystemEntity.isLink(path)) print('link'); // LINT +} + +Future some_FileSystemEntity_isLinkSync() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.isLinkSync(path)) print('link'); // OK +} + +Future some_FileSystemEntity_type() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.type(path) == null) {} // LINT +} + +Future some_FileSystemEntity_type2() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.type(path, followLinks: true) == null) {} // LINT +} + +Future some_FileSystemEntity_typeSync() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.typeSync(path) == null) {} // OK +} + +Future some_FileSystemEntity_typeSync2() async { + String path = '/path/to/my/file/entity'; + if (FileSystemEntity.typeSync(path, followLinks: true) == null) {} // OK +} + +Future some_Directory_exists() async { + var dir = Directory('/path/to/my/dir'); + if (await dir.exists()) print('exists'); // LINT +} + +Future some_Directory_existsSync() async { + var dir = Directory('/path/to/my/dir'); + if (dir.existsSync()) print('before'); // OK +} + +Future some_Directory_stat() async { + var dir = Directory('/path/to/my/file'); + if (await dir.stat() == null) print('stat'); // LINT +} + +Future some_Directory_statSync() async { + var dir = Directory('/path/to/my/dir'); + if (dir.statSync() == null) print('stat'); // OK +} diff --git a/pkg/linter/test_data/rules/avoid_type_to_string.dart b/pkg/linter/test_data/rules/avoid_type_to_string.dart new file mode 100644 index 00000000000..79864fb345c --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_type_to_string.dart @@ -0,0 +1,153 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_type_to_string` + +// SHARED + +class A { + String toString() => ''; +} + +String takesFunction(Function f) => ''; + +class TypeChildWithOverride extends Type { + @override + String toString() => ''; +} + +class TypeGrandChildWithOverride extends TypeChildWithOverride {} + +class TypeChildNoOverride extends Type {} + +class TypeGrandChildNoOverride extends TypeChildNoOverride {} + +mixin ToStringMixin { + String toString() => ''; +} + +// BAD + +class Bad { + void doBad(Function f) { + A().runtimeType.toString(); // LINT + TypeChildNoOverride().toString(); // LINT + TypeGrandChildNoOverride().toString(); // LINT + } +} + +class BadWithType extends Type { + Function passedFunction = (){}; + + BadWithType(Function func) : this.withFunc(func); + BadWithType.withoutFunc() {} + BadWithType.withFunc(this.passedFunction) {} + BadWithType.withSelf(BadWithType badWithType) + : this.withFunc(badWithType.toString); // LINT + + void doBad() { + toString(); // LINT + this.toString(); // LINT + + print('${toString()}'); // LINT + print('${this.toString()}'); // LINT + print('${takesFunction(toString)}'); // LINT + print('${takesFunction(this.toString)}'); // LINT + + takesFunction(toString); // LINT + takesFunction(this.toString); // LINT + takesFunction(BadWithType.withoutFunc().toString); // LINT + Bad().doBad(toString); // LINT + Bad().doBad(this.toString); // LINT + Bad().doBad(BadWithType.withoutFunc().toString); // LINT + + BadWithType(toString); // LINT + BadWithType.withFunc(this.toString); // LINT + + ((Function internal) => internal())(toString); // LINT + } +} + +class BadWithTypeChild extends BadWithType { + BadWithTypeChild(BadWithType badWithType) + : super(badWithType.toString); // LINT + BadWithTypeChild.redirect(BadWithType badWithType) + : super.withFunc(badWithType.toString); // LINT +} + +mixin callToStringOnBadWithType on BadWithType { + void mixedBad() { + toString(); // LINT + this.toString(); // LINT + } +} + +extension ExtensionOnBadWithType on BadWithType { + void extendedBad() { + toString(); // LINT + this.toString(); // LINT + } +} + +// GOOD + +class Good { + void doGood() { + toString(); // OK + A().toString(); // OK + TypeChildWithOverride().toString(); // OK + TypeGrandChildWithOverride().toString(); // OK + + final refToString = toString; + refToString(); // OK? + takesFunction(refToString); // OK + } +} + +class GoodWithType extends Type { + Function passedFunction; + + GoodWithType.withFunc(this.passedFunction) {} + GoodWithType.withSelf(GoodWithTypeAndMixin goodWithTypeAndMixin) + : this.withFunc(goodWithTypeAndMixin.toString); // OK + GoodWithType.withOther(Good good) : this.withFunc(good.toString); // OK + + void good() { + String toString() => ''; + toString(); // OK + } +} + +class GoodWithTypeAndMixin extends Type with ToStringMixin { + void doGood() { + toString(); // OK + this.toString(); // OK + + takesFunction(toString); // OK + takesFunction(this.toString); // OK + takesFunction(GoodWithTypeAndMixin().toString); // OK + } +} + +mixin CallToStringOnGoodWithType on GoodWithTypeAndMixin { + void mixedGood() { + toString(); // OK + this.toString(); // OK + } +} + +extension ExtensionOnGoodWithTypeAndMixin on GoodWithTypeAndMixin { + void extendedGood() { + toString(); // OK + this.toString(); // OK + } +} + +extension on int Function(int) { + // ignore: unused_element + void extendedGood() { + toString(); // OK + this.toString(); // OK + } +} diff --git a/pkg/linter/test_data/rules/avoid_types_as_parameter_names.dart b/pkg/linter/test_data/rules/avoid_types_as_parameter_names.dart new file mode 100644 index 00000000000..3d4ce344505 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_types_as_parameter_names.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_types_as_parameter_names` + +class SomeType {} + +void f() { + try { + // ignore: avoid_catches_without_on_clauses + } catch(SomeType) { //LINT + // ... + } +} + +typedef void f1(); // OK +typedef void f2(int a); // OK +typedef void f3(int); // LINT +typedef void f4( + num a, // OK + { + int, // LINT +}); +typedef void f5( + double a, // OK + [ + bool, // LINT +]); +typedef f6 = int Function(int); // OK +typedef void f7(Undefined); // OK + +m1(f()) => null; // OK +m2(f(int a)) => null; // OK +m3(f(int)) => null; // LINT +m4(f(num a, {int})) => null; // LINT +m5(f(double a, [bool])) => null; // LINT +m6(int Function(int) f)=> null; // OK +m7(f(Undefined)) => null; // OK +m8(f6) => null; // LINT +m9(f7) => null; // LINT +m10(m1) => null; // OK + +/// Naming the field `num` is significant - should be the name of a class. +class FieldFormalParameter { + final int num; + FieldFormalParameter(this.num); // OK +} + +void m11(X) {} // LINT + +void m12(int Function(T) g) {} // OK + +class C { + C.name(); + factory C(X) => C.name(); // LINT + void m(void Function(X) h) {} // OK +} diff --git a/pkg/linter/test_data/rules/avoid_unnecessary_containers.dart b/pkg/linter/test_data/rules/avoid_unnecessary_containers.dart new file mode 100644 index 00000000000..effddceba4b --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_unnecessary_containers.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_unnecessary_containers` + +// ignore_for_file: prefer_expression_function_bodies + +import 'package:flutter/widgets.dart'; + +Widget w() { + return Container( // LINT + child: Row(), + ); +} + +Widget ww() { + return Container( // OK + child: Row(), + width: 10, + height: 10, + ); +} + +Widget www() { + return Container( // OK + ); +} diff --git a/pkg/linter/test_data/rules/avoid_unused_constructor_parameters.dart b/pkg/linter/test_data/rules/avoid_unused_constructor_parameters.dart new file mode 100644 index 00000000000..5c573e683d2 --- /dev/null +++ b/pkg/linter/test_data/rules/avoid_unused_constructor_parameters.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_unused_constructor_parameters` + +class A { + A(); // OK +} + +class B { + int a = 0; + int b = 0; + + B(this.a, [this.b = 0]); // OK because field formal parameters are being used +} + +class C { + int a; + int b; + + C({this.a = 0, this.b = 0}); // OK because field formal parameters are being used +} + +class D { + D(int a, // LINT + [int b = 5]); // LINT +} + +class E { + int c = 0; + + E(int a, {int b = 10}) { // OK because all parameters are used + c = a + b; + } +} + +class F { + int n = 0; + + F(int a, [int b = 10, // LINT + int c = 42]) { // LINT + n = a + 42; + } +} + +class G { + int c = 0; + int d = 0; + + G(int a, {int b = 0, this.c = 0}) { // OK because all non-field-formal parameters are used + d = a + b; + } +} + +class H { + int c; + + H(int a, int b) : c = a + b; // OK because parameters are used in initializer +} + +class I extends H { + I(int a, int b) : super(a, b); // OK because parameters are used in initializer +} + +class J extends H { + int d = 0; + + J(int a, int b, int c) : super(a, b) { // OK because all parameters are used + d = a * b * c; + } +} + +class K { + int a = 0; + int b = 0; + + K(this.a, {this.b = 0, int c = 0}); // LINT +} + +class L { + int c; + + L(int a, int b) : c = a + b; + L.named(int a, int b, int c) : this(a, b); // LINT +} + +class M { + M._internal(int n); // LINT + + factory M(int a, int b) => M._internal(a); // LINT + factory M.redirect(int n) = M._internal; // OK because target constructor have parameters +} + +class N { + external N(int n); // OK + external factory N.named(int n); // OK +} + +class O { + O(@Deprecated('') int x); // OK because the parameter is deprecated +} + +class P { + P(int _, Object __); // OK by naming convention: https://github.com/dart-lang/linter/issues/1793 +} diff --git a/pkg/linter/test_data/rules/await_only_futures.dart b/pkg/linter/test_data/rules/await_only_futures.dart new file mode 100644 index 00000000000..727719bbc60 --- /dev/null +++ b/pkg/linter/test_data/rules/await_only_futures.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N await_only_futures` + +import 'dart:async'; + +bad() async { + print(await 23); // LINT +} + +good() async { + print(await new Future.value(23)); +} + +Future awaitWrapper(dynamic future) async { + return await future; // OK +} + +class CancellableFuture implements Future { + @override + Stream asStream() { + throw new Exception('Not supported.'); + } + + @override + Future catchError(Function onError, {bool test(Object error)?}) { + throw new Exception('Not supported.'); + } + + + @override + Future timeout(Duration timeLimit, {onTimeout()?}) { + throw new Exception('Not supported.'); + } + + @override + Future whenComplete(action()) { + throw new Exception('Not supported.'); + } + + @override + Future then(FutureOr Function(T value) onValue, {Function? onError}) { + throw new Exception('Not supported.'); + } +} + +Future awaitCancellableFuture(dynamic future) async { + return await new CancellableFuture(); // OK +} + +Future awaitFutureOr(FutureOr callback()) async { + return await callback(); // OK +} + +allow_await_null() async { + await null; // OK +} diff --git a/pkg/linter/test_data/rules/camel_case_types.dart b/pkg/linter/test_data/rules/camel_case_types.dart new file mode 100644 index 00000000000..ae8d4b6ee32 --- /dev/null +++ b/pkg/linter/test_data/rules/camel_case_types.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N camel_case_types` + +class A {} + +class DB {} + +class FooBar {} + +class _Foo {} + +class Foo extends _Foo {} + +typedef bool predicate(); //LINT [14:9] + +typedef bool Predicate(); //OK + +class fooBar // LINT +{} + +class Foo$Bar //OK +{} + +class Foo_Bar //LINT +{} + +class $FooBar //OK +{} + +typedef foo = Foo; //LINT + +typedef F = Foo; //OK + +typedef f = void Function(); //LINT + +mixin M {} +class c = Object with M; //LINT + +enum foooBar { a } //LINT + +enum FoooBar { a } //OK diff --git a/pkg/linter/test_data/rules/cascade_invocations.dart b/pkg/linter/test_data/rules/cascade_invocations.dart new file mode 100644 index 00000000000..8212f8857c8 --- /dev/null +++ b/pkg/linter/test_data/rules/cascade_invocations.dart @@ -0,0 +1,252 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N cascade_invocations` + +import 'dart:math' as math; + +void alreadyDeclared() { + List list; + print("intermediate statement"); + list = []; + list.clear(); // LINT +} + +void noCascade() { + List list = []; + list.clear(); // LINT + list.clear(); // LINT +} + +int get someThing => 5; + +void noCascadeIntermediate() { + List list = []; + print(list); + list.clear(); + list.clear(); // LINT + list.toString(); // LINT + // toString is not void and it is not a problem. +} + +void cascadeIntermediate() { + List list = []; + print(list); + list.clear(); + list.clear(); // LINT + list..clear(); // LINT + list..clear(); // LINT + list..clear(); // LINT +} + +class HasMethodWithNoCascade { + List list = []; + + void noCascade() { + list.clear(); + list.clear(); // LINT + } +} + +class Foo { + int get bar => 5; + set bar(int value) {} + void baz() {} + void foo() {} +} + +void noCascadeWithGetter() { + final foo = Foo(); + foo.baz(); // LINT + foo.bar; // LINT + foo.foo(); // LINT + foo.bar = 8; // LINT + foo?.bar = 8; // OK + foo.bar = 0; // OK + foo?.bar; // OK +} + +void alternatingReferences() { + final foo = new Foo(); + final bar = new Foo(); + foo.baz(); + bar.baz(); + foo.bar; + bar.bar; + foo.foo(); + bar.foo(); +} + +void withDifferentTypes() { + Foo foo = new Foo(); + String nothing = ''; + foo.baz(); + print(nothing); + foo.foo(); + if (foo.bar > 5) {} +} + +void cascade() { + final foo = new Foo(); + foo?.baz(); + foo.baz(); // OK + foo.baz(); // LINT +} + +void prefixLibrary() { + math.sin(0); + math.cos(0); +} + +class StaticFoo { + static int increment() => 0; + static int decrement() => 0; +} + +void cascadeStatic() { + StaticFoo.increment(); + StaticFoo.decrement(); +} + +// https://github.com/dart-lang/linter/issues/339 +class Identifier339 { + String value = ''; + String system = ''; +} + +class Resource339 { + String id = ''; + dynamic identifier; +} + +class Practitioner339 extends Resource339 { + int foo = 0; + + String build() => ''; + + void bar(dynamic) {} +} + +class Entry339 { + Resource339 resource = Resource339(); +} + +void main339() { + final entry = new Entry339(); + final resource = (entry.resource as Practitioner339); + resource.identifier = [ // OK + new Identifier339() + ..system = 'urn:system' + ..value = 'mdc_${resource.id}' + ]; +} + +void otherDependencies1() { + final entry = new Entry339(); + final resource = (entry.resource as Practitioner339); + resource.identifier = resource; // OK +} + +void otherDependencies2() { + final entry = new Entry339(); + final resource = (entry.resource as Practitioner339); + resource.bar(resource); // OK +} + +// assignment +void otherDependencies3() { + var entry, resource; + entry = new Entry339(); + resource = (entry.resource as Practitioner339); + resource.bar(resource); // OK +} + +class Bug661 { + late Bug661 parent; + late Practitioner339 practitioner; + + void bar() { + practitioner.bar(1); + practitioner.bar(2); // LINT + parent.practitioner.bar(3); + } +} + +class Bug701 { + int foo = 0; + int bar = 0; +} + +void function701({Bug701? something}) { + something ??= new Bug701(); + something.foo = 1; // OK + something.bar = 2; // LINT +} + +// https://github.com/dart-lang/linter/issues/694 +class Bug694 { + static int foo = 0; + static int bar = 1; +} + +void function694() { + Bug694.bar = 2; + Bug694.foo = 3; // OK +} + +class A { + late int f; + late int g; + + int get p => 7; + int get q => 6; + int m() => 5; + void set s(int value) {} +} + +class B { + final A a = A(); + + B operator +(other) { + return B(); + } +} + +// https://github.com/dart-lang/linter/issues/1323 +void bug1323() { + final B b1 = B(); + final B b2 = B(); + + b1.a + ..f = 1 + ..g = 1; + b2.a.f = 2; // OK + + print('buffer statement.'); + + b1.a + ..p + ..q; + b2.a.p; // OK + + b1.a + ..p + ..q; + b2.a..p; // OK + + b1.a + ..p + ..m(); + b2.a.m(); // OK + + b1.a + ..p + ..s = 1; + b2.a.s = 1; // OK +} + +void bug1317() async { + final list = await Future.value([]); + list.forEach(print); // OK +} diff --git a/pkg/linter/test_data/rules/cast_nullable_to_non_nullable.dart b/pkg/linter/test_data/rules/cast_nullable_to_non_nullable.dart new file mode 100644 index 00000000000..adc576abff4 --- /dev/null +++ b/pkg/linter/test_data/rules/cast_nullable_to_non_nullable.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N cast_nullable_to_non_nullable` + +class A {} + +class B extends A {} + +m() { + var v; + { + A? a; + v = a as B; // LINT + } + { + A? a; + v = a as B?; // OK + } + { + A? a; + v = a as A; // LINT + } + + // exclude dynamic + { + dynamic b; + v = b as B; // OK + } + { + dynamic b; + v = b as B?; // OK + } +} diff --git a/pkg/linter/test_data/rules/combinators_ordering.dart b/pkg/linter/test_data/rules/combinators_ordering.dart new file mode 100644 index 00000000000..afd6ee35768 --- /dev/null +++ b/pkg/linter/test_data/rules/combinators_ordering.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N combinators_ordering` + +import 'dart:math' as m1 show max, min; // OK +import 'dart:math' as m2 show min, max; // LINT + +export 'dart:math' show max, min; // OK +export 'dart:math' show min, max; // LINT + +import 'dart:math' as m3 hide max, min; // OK +import 'dart:math' as m4 hide min, max; // LINT + +export 'dart:math' hide max, min; // OK +export 'dart:math' hide min, max; // LINT diff --git a/pkg/linter/test_data/rules/constant_identifier_names.dart b/pkg/linter/test_data/rules/constant_identifier_names.dart new file mode 100644 index 00000000000..4caf5bea686 --- /dev/null +++ b/pkg/linter/test_data/rules/constant_identifier_names.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N constant_identifier_names` + +const DEFAULT_TIMEOUT = 1000; //LINT +const PI = 3.14; //LINT + +class Z { + static const DEBUG = false; //LINT +} + +enum Foo { + bar, + Baz, //LINT +} diff --git a/pkg/linter/test_data/rules/control_flow_in_finally.dart b/pkg/linter/test_data/rules/control_flow_in_finally.dart new file mode 100644 index 00000000000..8a92735f0b6 --- /dev/null +++ b/pkg/linter/test_data/rules/control_flow_in_finally.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N control_flow_in_finally` + +class Ok { + double compliantMethod() { + var i = 5.0; + try { + i = 1 / 0; + } catch (e) { + print(e); + } finally { + i = i * i; // OK + } + return i; + } +} + +class BadReturn { + double nonCompliantMethod() { + try { + return 1 / 0; + } catch (e) { + print(e); + } finally { + return 1.0; // LINT + } + } +} + +class GoodReturn { + double? compliantMethod() { + try { + return 1 / 0; + } catch (e) { + print(e); + } finally { + () { + return 1.0; // OK + }; + } + } +} + +class BadContinue { + double nonCompliantMethod() { + for (var o in [1, 2]) { + try { + print(o / 0); + } catch (e) { + print(e); + } finally { + continue; // LINT + } + } + return 1.0; + } +} + +class GoodContinue { + double compliantMethod() { + try { + print(1 / 0); + } catch (e) { + print(e); + } finally { + for (var o in [1, 2]) { + print(o); + if (1 > 0) { + continue; // OK + } + } + } + return 1.0; + } +} + +class BadBreak { + double nonCompliantMethod() { + for (var o in [1, 2]) { + try { + print(o / 0); + } catch (e) { + print(e); + } finally { + if (1 > 0) { + break; // LINT + } else { + print('should catch nested cases!'); + } + } + } + return 1.0; + } +} + +class GoodBreak { + double compliantMethod() { + try { + print(1 / 0); + } catch (e) { + print(e); + } finally { + for (var o in [1, 2]) { + print(o); + if (1 > 0) { + break; // OK + } + } + } + return 1.0; + } +} diff --git a/pkg/linter/test_data/rules/curly_braces_in_flow_control_structures.dart b/pkg/linter/test_data/rules/curly_braces_in_flow_control_structures.dart new file mode 100644 index 00000000000..fb44e541425 --- /dev/null +++ b/pkg/linter/test_data/rules/curly_braces_in_flow_control_structures.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N curly_braces_in_flow_control_structures` + +testIfElse() { + if (false) return; // OK + + if (false) {} // OK + + if (false) + return; // LINT + + if (false) { // OK + } + + if (false) + return; // LINT + else return; // LINT + + if (false) { + } + else if (false) { // OK + } + else { + } + + if (false) + return; // LINT + else if (false) + return; // LINT + else + return; // LINT + + if (false) { // OK + } else if (false) { // OK + } else { // OK + } + + if (false) { } // OK + else return; // LINT + + if (false) + return; // LINT + else + return; // LINT + + if (false){ }// OK + else {} // OK + + if (false) print( // LINT + 'First argument' + 'Second argument'); + + if (false) { print('should be on next line'); // OK + } + + if (true) { + } else if (true) return; //LINT + + if (true) { + } else if (true) + return; //LINT +} + +testWhile() { + while (true) return; // LINT + + while (true) {} // OK + + while (true) + return; // LINT +} + +testForEach(List l) { + for (var i in l) return; // LINT + + for (var i in l) {} // OK + + for (var i in l) + return; // LINT +} + +testFor() { + for (;;) return; // LINT + + for (;;) {} // OK + + for (;;) + return; // LINT +} + +testDo() { + do print(''); while (true); // LINT + do print(''); // LINT + while (true); + + do + print(''); // LINT + while (true); + + do {print('');} // OK + while (true); +} diff --git a/pkg/linter/test_data/rules/deprecated_consistency.dart b/pkg/linter/test_data/rules/deprecated_consistency.dart new file mode 100644 index 00000000000..5667838fd3a --- /dev/null +++ b/pkg/linter/test_data/rules/deprecated_consistency.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N deprecated_consistency` + +@deprecated +class A { + A.ko(); // LINT + @deprecated + A.ok(); // OK +} + +class B { + B.ko({this.field = 0}); // LINT + B.ok({@deprecated this.field = 0}); // OK + + @deprecated + Object field; +} + +class C { + C({@deprecated this.field = 0}); // OK + + Object field; // LINT +} diff --git a/pkg/linter/test_data/rules/diagnostic_describe_all_properties.dart b/pkg/linter/test_data/rules/diagnostic_describe_all_properties.dart new file mode 100644 index 00000000000..7adc5683f49 --- /dev/null +++ b/pkg/linter/test_data/rules/diagnostic_describe_all_properties.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N diagnostic_describe_all_properties` + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +class MyWidget extends Diagnosticable { + Widget? p0; //Skipped + List p00 = []; //Skipped + Widget? get p000 => null; //Skipped + String p1 = ''; //OK + String p2 = ''; //LINT + String get p3 => ''; //LINT + String _p3 = ''; //OK + String debugFoo = ''; //OK + String foo = ''; //OK (covered by debugFoo) + String debugBar = ''; //OK (covered by bar) + String bar = ''; //OK + static String p4 = ''; //OK + String p5 = ''; //OK (in debugDescribeChildren) + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + .add(StringProperty('property', p1, defaultValue: '', quoted: false)); + properties.add(StringProperty('debugFoo', debugFoo, + defaultValue: '', quoted: false)); + properties + .add(StringProperty('bar', bar, defaultValue: '', quoted: false)); + } + + @override + List debugDescribeChildren() { + // In real source this should be used to create a diagnostics node, + // but for us a reference suffices. + print(p5); + return []; + } +} + +class MyWidget2 extends Diagnosticable { + bool property = false; //LINT +} diff --git a/pkg/linter/test_data/rules/eol_at_end_of_file.dart b/pkg/linter/test_data/rules/eol_at_end_of_file.dart new file mode 100644 index 00000000000..0f56b6dbff4 --- /dev/null +++ b/pkg/linter/test_data/rules/eol_at_end_of_file.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N eol_at_end_of_file` + +bar() { +} //LINT \ No newline at end of file diff --git a/pkg/linter/test_data/rules/experiments/experiments.dart b/pkg/linter/test_data/rules/experiments/experiments.dart new file mode 100644 index 00000000000..081d8a5c30c --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/experiments.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:linter/src/analyzer.dart'; +import 'package:linter/src/rules/always_declare_return_types.dart'; +import 'package:linter/src/rules/avoid_returning_this.dart'; +import 'package:linter/src/rules/avoid_positional_boolean_parameters.dart'; +import 'package:linter/src/rules/camel_case_extensions.dart'; +import 'package:linter/src/rules/prefer_constructors_over_static_methods.dart'; +import 'package:linter/src/rules/public_member_api_docs.dart'; +import 'package:linter/src/rules/slash_for_doc_comments.dart'; +import 'package:linter/src/rules/type_annotate_public_apis.dart'; +import 'package:linter/src/rules/use_setters_to_change_properties.dart'; + +final experiments = [ + AlwaysDeclareReturnTypes(), + AvoidPositionalBooleanParameters(), + AvoidReturningThis(), + CamelCaseExtensions(), + PreferConstructorsInsteadOfStaticMethods(), + PublicMemberApiDocs(), + SlashForDocComments(), + TypeAnnotatePublicApis(), + UseSettersToChangeAProperty(), +]; + +void registerLintRuleExperiments() { + experiments.forEach(Analyzer.facade.register); +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/analysis_options.yaml b/pkg/linter/test_data/rules/experiments/nnbd/analysis_options.yaml new file mode 100644 index 00000000000..4163582097c --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + enable-experiment: + - non-nullable diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/avoid_returning_null.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/avoid_returning_null.dart new file mode 100644 index 00000000000..4f14c7099aa --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/avoid_returning_null.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N avoid_returning_null` + +/// See: https://github.com/dart-lang/linter/issues/2636 + +int? getFoo() => null; // OK diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/tighten_type_of_initializing_formals.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/tighten_type_of_initializing_formals.dart new file mode 100644 index 00000000000..6cdb3852bfc --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/tighten_type_of_initializing_formals.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N tighten_type_of_initializing_formals` + +class A { + A.c1(this.p) : assert(p != null); // LINT + A.c2(String this.p) : assert(p != null); // OK + A.c3(this.p); // OK + A.c4(this.p) : assert(null != p); // LINT + A.c5(String this.p) : assert(null != p); // OK + A.c6( + this.p1, // LINT + String? p2, // OK + this.p3, { // OK + this.p4, // LINT + this.p5, // OK + }) : assert(p1 != null), + assert(p2 != null), + assert(p4 != null); + + String? p; + String? p1; + String? p2; + String? p3; + String? p4; + String? p5; +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/type_init_formals.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/type_init_formals.dart new file mode 100644 index 00000000000..185c3cacbe9 --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/type_init_formals.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N type_init_formals` + +class A { + String? p1; + String p2 = ''; + + A.w({required String this.p1}); // OK + A.x({required String? this.p1}); // LINT + A.z({required String this.p2}); // LINT +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_null_in_if_null_operators.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_null_in_if_null_operators.dart new file mode 100644 index 00000000000..99b3605e602 --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_null_in_if_null_operators.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_null_in_if_null_operators` + +var x = 1 ?? null; //LINT +var y = 1 ?? 1; //OK +var z = null ?? 1; //LINT + +class X { + m1() { + var x = 1 ?? null; //LINT + var y = 1 ?? 1; //OK + var z = null ?? 1; //LINT + } +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_nullable_for_final_variable_declarations.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_nullable_for_final_variable_declarations.dart new file mode 100644 index 00000000000..c1beee8a8bd --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/unnecessary_nullable_for_final_variable_declarations.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_nullable_for_final_variable_declarations` + +final int? _i = 1; // LINT +final int _j = 1; // OK +final int? i = 1; // LINT +final int j = 1; // OK +const int? ic = 1; // LINT +const int jc = 1; // OK +const dynamic jcd = 1; // OK + +class A { + final int? _i = 1; // LINT + final int _j = 1; // OK + final int? i = 1; // OK (may be overriden or may override) + final int j = 1; // OK + final dynamic jd = 1; // OK + static final int? si = 1; // LINT + static final int sj = 1; // OK +} + +extension E on A { + static final int? _e1i = 1; // LINT + static final int _e1j = 1; // OK + static final int? e1i = 1; // LINT + static final int e1j = 1; // OK + static final dynamic e1jd = 1; // OK +} + +m() { + final int? _i = 1; // LINT + final int _j = 1; // OK + final int? i = 1; // LINT + final int j = 1; // OK + final dynamic jd = 1; // OK +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/unrelated_type_equality_checks.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/unrelated_type_equality_checks.dart new file mode 100644 index 00000000000..d4c98af8f58 --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/unrelated_type_equality_checks.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unrelated_type_equality_checks` + +void m(int? a1, num a2) { + var b1 = a1 == a2; // OK + var b2 = a2 == a1; // OK +} diff --git a/pkg/linter/test_data/rules/experiments/nnbd/rules/void_checks.dart b/pkg/linter/test_data/rules/experiments/nnbd/rules/void_checks.dart new file mode 100644 index 00000000000..2d2fedbbd64 --- /dev/null +++ b/pkg/linter/test_data/rules/experiments/nnbd/rules/void_checks.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N void_checks` + +import 'dart:async'; + +void emptyFunctionExpressionReturningFutureOrVoid(FutureOr Function() f) { + f = () {}; // OK +} + +Never fail() { throw 'nope'; } + +void m1() async { + await Future.value(5).then((x) { // OK + fail(); + }); +} + +// https://github.com/dart-lang/linter/issues/3172 +void capture(FutureOr Function() callback) {} + +void m2() { + capture(() { // OK + throw "oh no"; + }); +} diff --git a/pkg/linter/test_data/rules/hash_and_equals.dart b/pkg/linter/test_data/rules/hash_and_equals.dart new file mode 100644 index 00000000000..594542797ac --- /dev/null +++ b/pkg/linter/test_data/rules/hash_and_equals.dart @@ -0,0 +1,50 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N hash_and_equals` + +class Bad { + final int value; + Bad(this.value); + + @override + bool operator ==(Object other) => other is Bad && other.value == value; //LINT +} + +class Bad2 { + final int value; + Bad2(this.value); + + @override + int get hashCode => value.hashCode; //LINT +} + +class Better //OK! +{ + final int value; + Better(this.value); + + @override + bool operator ==(Object other) => other is Better && other.value == value; + + @override + int get hashCode => value.hashCode; +} + +class OK {} + +class AlsoOk { + @override + final int hashCode; + AlsoOk(this.hashCode); + + @override + bool operator ==(Object other) => other is AlsoOk && other.hashCode == hashCode; //OK +} + +class NotOk { + @override + final int hashCode; //LINT + NotOk(this.hashCode); +} diff --git a/pkg/linter/test_data/rules/implicit_call_tearoffs.dart b/pkg/linter/test_data/rules/implicit_call_tearoffs.dart new file mode 100644 index 00000000000..663f3ae5271 --- /dev/null +++ b/pkg/linter/test_data/rules/implicit_call_tearoffs.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N implicit_call_tearoffs` + +class C { + void call() {} + void other() {} +} + +class C2 { + void call(T arg) {} +} + +void callIt(void Function() f) { + f(); +} + +void callIt2(void Function(int) f) { + f(0); +} + +void Function() r1() => C(); // LINT +void Function() r2() => C().call; // OK +void Function() r3(C c) => c; // LINT +void Function() r4(C c) => c.call; // OK + +void Function() r5(C? c1, C c2) { + return c1 ?? c2; // LINT +} + +void Function() r6(C? c1, C c2) { + return c1?.call ?? c2.call; // OK +} + +void Function() r7() { + return C()..other(); // LINT +} + +void Function() r8() { + return (C()..other()).call; // OK +} + +List r9(C c) { + return [c]; // LINT +} + +List r10(C c) { + return [c.call]; // OK +} + +void Function(int) r11(C2 c) => c; // LINT +void Function(int) r12(C2 c) => c.call; // OK + +void main() { + callIt(C()); // LINT + callIt(C().call); // OK + Function f1 = C(); // LINT + Function f2 = C().call; // OK + void Function() f3 = C(); // LINT + void Function() f4 = C().call; // OK + + final c = C(); + callIt(c); // LINT + callIt(c.call); // OK + Function f5 = c; // LINT + Function f6 = c.call; // OK + void Function() f7 = c; // LINT + void Function() f8 = c.call; // OK + + [ + C(), // LINT + C().call, //OK + c, // LINT + c.call, // OK + ]; + + callIt2(C2()); // LINT + callIt2(C2().call); // OK + callIt2(C2()); // LINT + callIt2(C2().call); // OK + Function f9 = C2(); // LINT + Function f10 = C2().call; // OK + Function f11 = C2(); // LINT + Function f12 = C2().call; // OK + void Function(T) f13 = C2(); // LINT + void Function(T) f14 = C2().call; // OK + void Function(int) f15 = C2(); // LINT + void Function(int) f16 = C2().call; // OK + void Function(int) f17 = C2(); // LINT + void Function(int) f18 = C2().call; // OK + + final c2 = C2(); + callIt2(c2); // LINT + callIt2(c2.call); // OK + callIt2(c2); // LINT + callIt2(c2.call); // OK + Function f19 = c2; // LINT + Function f20 = c2.call; // OK + Function f21 = c2; // LINT + Function f22 = c2.call; // OK + void Function(T) f23 = c2; // LINT + void Function(T) f24 = c2.call; // OK + void Function(int) f25 = c2; // LINT + void Function(int) f26 = c2.call; // OK + void Function(int) f27 = c2; // LINT + void Function(int) f28 = c2.call; // OK + + [ + C2(), // LINT + C2().call, //OK + C2(), // LINT + C2().call, //OK + c2, // LINT + c2.call, // OK + c2, // LINT + c2.call, // OK + ]; + + C2(); // LINT + c2; // LINT +} diff --git a/pkg/linter/test_data/rules/iterable_contains_unrelated_type.dart b/pkg/linter/test_data/rules/iterable_contains_unrelated_type.dart new file mode 100644 index 00000000000..fa05527cca9 --- /dev/null +++ b/pkg/linter/test_data/rules/iterable_contains_unrelated_type.dart @@ -0,0 +1,208 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N iterable_contains_unrelated_type` + +// TODO(https://github.com/dart-lang/linter/issues/4143): Use 3.0.0 +// @dart=2.19 + +void someFunction() { + var list = []; + if (list.contains('1')) print('someFunction'); // LINT + list + ..add(1) + ..contains('1'); // LINT +} + +void someFunction1() { + var list = []; + if (list.contains(1)) print('someFunction1'); // OK +} + +void someFunction3() { + List list = []; + if (list.contains('1')) print('someFunction3'); // LINT +} + +void someFunction4() { + List list = []; + if (list.contains(1)) print('someFunction4'); // OK +} + +void someFunction4_1() { + List list = []; + if (list.contains(null)) print('someFunction4_1'); +} + +void someFunction5_1() { + List list = []; + Object instance = ''; + if (list.contains(instance)) print('someFunction5_1'); // OK +} + +void someFunction5() { + List list = []; + DerivedClass1 instance = DerivedClass1(); + if (list.contains(instance)) print('someFunction5'); // OK +} + +void someFunction6() { + List list = []; + DerivedClass2 instance = DerivedClass2(); + if (list.contains(instance)) print('someFunction6'); // OK +} + +class MixedIn with Mixin {} + +void someFunction6_1() { + List list = []; + Mixin instance = MixedIn(); + if (list.contains(instance)) print('someFunction6_1'); // OK +} + +void someFunction7() { + List list = []; + DerivedClass3 instance = DerivedClass3(); + if (list.contains(instance)) print('someFunction7'); // OK +} + +void someFunction7_1() { + List list = []; + Mixin instance = MixedIn(); + if (list.contains(instance)) print('someFunction7_1'); // OK +} + +void someFunction8() { + List list = []; + DerivedClass3 instance = DerivedClass3(); + if (list.contains(instance)) print('someFunction8'); // OK +} + +void someFunction9() { + List list = []; + Abstract instance = new Abstract(); + if (list.contains(instance)) print('someFunction9'); // OK +} + +void someFunction10() { + var list = []; + if (list.contains(1)) print('someFunction10'); // OK +} + +void someFunction11(unknown) { + var what = unknown - 1; + List list = []; + list.forEach((int i) { + if (what == i) print('someFunction11'); // OK + }); +} + +void someFunction12() { + List list = []; + DerivedClass5 instance = DerivedClass5(); + if (list.contains(instance)) print('someFunction12'); // LINT +} + +void bug_267(list) { + if (list.contains('1')) // https://github.com/dart-lang/linter/issues/267 + print('someFunction'); +} + +abstract class ClassBase {} + +class DerivedClass1 extends ClassBase {} + +abstract class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} + +abstract class Abstract { + factory Abstract() { + return new Implementation(); + } + + Abstract._internal(); +} + +class Implementation extends Abstract { + Implementation() : super._internal(); +} + +class DerivedClass4 extends DerivedClass2 {} + +class DerivedClass5 extends DerivedClass3 {} + +bool takesIterable(Iterable iterable) => iterable.contains('a'); // LINT + +bool takesIterable2(Iterable iterable) => iterable.contains('a'); // OK + +bool takesIterable3(Iterable iterable) => iterable.contains('a'); // OK + +abstract class A implements Iterable {} + +abstract class B extends A {} + +bool takesB(B b) => b.contains('a'); // LINT + +abstract class A1 implements Iterable {} + +abstract class B1 extends A1 {} + +bool takesB1(B1 b) => b.contains('a'); // OK + +abstract class A3 implements Iterable {} + +abstract class B3 extends A3 {} + +bool takesB3(B3 b) => b.contains('a'); // OK + +abstract class AddlInterface {} + +abstract class SomeIterable implements Iterable, AddlInterface {} + +abstract class MyClass implements SomeIterable { + bool badMethod(String thing) => this.contains(thing); // LINT + bool badMethod1(String thing) => contains(thing); // LINT +} + +abstract class MyDerivedClass extends MyClass { + bool myConcreteBadMethod(String thing) => this.contains(thing); // LINT + bool myConcreteBadMethod1(String thing) => contains(thing); // LINT +} + +abstract class MyMixedClass extends Object with MyClass { + bool myConcreteBadMethod(String thing) => this.contains(thing); // LINT + bool myConcreteBadMethod1(String thing) => contains(thing); // LINT +} + +abstract class MyIterableMixedClass extends Object + with MyClass + implements Iterable { + bool myConcreteBadMethod(String thing) => this.contains(thing); // LINT + bool myConcreteBadMethod1(String thing) => contains(thing); // LINT +} + +enum E implements List { + one, + two; + + @override + dynamic noSuchMethod(_) => throw UnsupportedError(''); + + void f() { + contains('string'); // LINT + this.contains('string'); // LINT + contains(1); // OK + } +} + +extension on List { + void f() { + contains('string'); // LINT + this.contains('string'); // LINT + contains(1); // OK + } +} diff --git a/pkg/linter/test_data/rules/library_prefixes.dart b/pkg/linter/test_data/rules/library_prefixes.dart new file mode 100644 index 00000000000..e1672d01cec --- /dev/null +++ b/pkg/linter/test_data/rules/library_prefixes.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N library_prefixes` + +import 'dart:async' as _async; //OK +import 'dart:collection' as $collection; //OK +import 'dart:convert' as _1; //LINT +import 'dart:core' as _i1; //OK +import 'dart:math' as dartMath; //LINT [23:8] + +main() { + _i1.print(dartMath.pi); + _i1.print(_async.Timer); + _i1.print(new $collection.LinkedHashSet<_i1.String>()); + _i1.print(_1.json); +} diff --git a/pkg/linter/test_data/rules/library_private_types_in_public_api.dart b/pkg/linter/test_data/rules/library_private_types_in_public_api.dart new file mode 100644 index 00000000000..dc2798340d7 --- /dev/null +++ b/pkg/linter/test_data/rules/library_private_types_in_public_api.dart @@ -0,0 +1,125 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N library_private_types_in_public_api` + +// Top level functions + +String f1(int i) => ''; +_Private1 f2(int i) => _Private1.fromInt(0); // LINT +String f3(_Private1 p) => ''; // LINT +_Private1 _f4(_Private1 p) => _Private1.fromInt(0); + +// Top level variables + +String v1 = ''; +_Private1? v2; // LINT +_Private2? v3; // LINT +List<_Private1> v4 = []; // LINT +_Private1 _v5 = _Private1.fromInt(0); + +// Top level getters + +String get g1 => ''; +_Private1 get g2 => _Private1.fromInt(0); // LINT +_Private1 get _g3 => _Private1.fromInt(0); + +// Top level setters + +set s1(int i) {} +set s2(_Private1 i) {} // LINT +set _s3(_Private1 i) {} + +// Type aliases + +typedef _Private1 F1(); // LINT +typedef void F2(_Private1 p); // LINT +typedef F3 = _Private1 Function(); // LINT +typedef F4 = void Function(_Private1); // LINT +typedef String F5(); +typedef F6 = void Function(int); + +// Classes + +class Public1 {} + +class Public2 extends Public4<_Private1> {} + +class Public3 extends Object with _PrivateMixin {} + +class Public4 implements _Private2 {} + +class _Private1 { + _Private1.fromInt(int i); + _Private1 f = _Private1.fromInt(0); + _Private1 m1(_Private1 p) => _Private1.fromInt(0); +} + +class _Private2 {} + +mixin _PrivateMixin {} + +// Mixins + +mixin Public5 on Public1 implements Public6 {} + +mixin Public6 on _Private1 {} // LINT + +mixin Public7 implements _Private1 {} + +mixin _Private3 on _Private1 implements _Private2 {} + +// Extensions + +extension Public8 on Public1 {} + +extension Public9 on _Private1 {} // LINT + +extension on String {} + +extension _E on int {} + +// Class members + +class PublicClassWithMembers { + // Fields + + String f1 = ''; + _Private1 f2 = _Private1.fromInt(0); // LINT + _Private2 f3 = _Private2(); // LINT + List<_Private1> f4 = []; // LINT + _Private1 _f5 = _Private1.fromInt(0); + + // Constructors + + PublicClassWithMembers(_Private1 p); // LINT + PublicClassWithMembers.c1(_Private1 p); // LINT + PublicClassWithMembers._c2(_Private1 p); + + // Methods + + String m1(int i) => _m4(_Private1.fromInt(i)).toString(); + _Private1 m2(int i) => _Private1.fromInt(0); // LINT + String m3(_Private1 p) => ''; // LINT + _Private1 _m4(_Private1 p) => _Private1.fromInt(0); + + // Operators + + int operator+(_Private1 p) => 0; // LINT + _Private1 operator-(int i) => _Private1.fromInt(i); // LINT + + // Getters + + String get g1 => _g3.toString(); + _Private1 get g2 => _Private1.fromInt(0); // LINT + _Private1 get _g3 => _f5; + + // Setters + + set s1(int i) { + _s3 = _Private1.fromInt(i); + } + set s2(_Private1 i) {} // LINT + set _s3(_Private1 p) {} +} diff --git a/pkg/linter/test_data/rules/lines_longer_than_80_chars.dart b/pkg/linter/test_data/rules/lines_longer_than_80_chars.dart new file mode 100644 index 00000000000..4c9b80508c8 --- /dev/null +++ b/pkg/linter/test_data/rules/lines_longer_than_80_chars.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N lines_longer_than_80_chars` + +var short = 'This is a short line'; // OK +var line80 = + ' '; // OK +var line81 = + ' '; // LINT +var long = + 'This line is a long, very long, very very long, very very very long, very very very very long'; // LINT + +var uriIsOk = + 'https://dart.dev/guides/language/effective-dart/style#avoid-lines-longer-than-80-characters'; // OK + +var posixPathIsOk = + '/home/dart.dev/guides/language/effective-dart/style#avoid-lines-longer-than-80-characters'; // OK + +var windowsPathIsOk = + r'C:\home\dart.dev\guides\language\effective-dart\style\avoid-lines-longer-than-80-characters'; // OK + +var multilinesOK = ''' +This line is a long, very long, very very long, very very very long, very very very very long +'''; + +var interpolated = 'this $uriIsOk that'; + +var line80EndingWithCRLF = + ' '; // OK diff --git a/pkg/linter/test_data/rules/list_remove_unrelated_type.dart b/pkg/linter/test_data/rules/list_remove_unrelated_type.dart new file mode 100644 index 00000000000..204536057b6 --- /dev/null +++ b/pkg/linter/test_data/rules/list_remove_unrelated_type.dart @@ -0,0 +1,209 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N list_remove_unrelated_type` + +void someFunction() { + var list = []; + if (list.remove('1')) print('someFunction'); // LINT + list + ..add(1) + ..remove('1'); // LINT +} + +void someFunction1() { + var list = []; + if (list.remove(1)) print('someFunction1'); // OK +} + +void someFunction3() { + List list = []; + if (list.remove('1')) print('someFunction3'); // LINT +} + +void someFunction4() { + List list = []; + if (list.remove(1)) print('someFunction4'); // OK +} + +void someFunction4_1() { + List list = []; + if (list.remove(null)) print('someFunction4_1'); +} + +void someFunction5_1() { + List list = []; + Object instance = ''; + if (list.remove(instance)) print('someFunction5_1'); // OK +} + +void someFunction5() { + List list = []; + DerivedClass1 instance = DerivedClass1(); + if (list.remove(instance)) print('someFunction5'); // OK +} + +void someFunction6() { + List list = []; + DerivedClass2 instance = DerivedClass2(); + if (list.remove(instance)) print('someFunction6'); // OK +} + +class MixedIn with Mixin {} + +void someFunction6_1() { + List list = []; + Mixin instance = MixedIn(); + if (list.remove(instance)) print('someFunction6_1'); // OK +} + +void someFunction7() { + List list = []; + DerivedClass3 instance = DerivedClass3(); + if (list.remove(instance)) print('someFunction7'); // OK +} + +void someFunction7_1() { + List list = []; + Mixin instance = MixedIn(); + if (list.remove(instance)) print('someFunction7_1'); // OK +} + +void someFunction8() { + List list = []; + DerivedClass3 instance = DerivedClass3(); + if (list.remove(instance)) print('someFunction8'); // OK +} + +void someFunction9() { + List list = []; + Abstract instance = new Abstract(); + if (list.remove(instance)) print('someFunction9'); // OK +} + +void someFunction10() { + var list = []; + if (list.remove(1)) print('someFunction10'); // OK +} + +void someFunction11(unknown) { + var what = unknown - 1; + List list = []; + list.forEach((int i) { + if (what == i) print('someFunction11'); // OK + }); +} + +void someFunction12() { + List list = []; + DerivedClass5 instance = DerivedClass5(); + if (list.remove(instance)) print('someFunction12'); // LINT +} + +void bug_267(list) { + if (list.remove('1')) + print('someFunction'); // https://github.com/dart-lang/linter/issues/267 +} + +abstract class ClassBase {} + +class DerivedClass1 extends ClassBase {} + +mixin class Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} + +abstract class Abstract { + factory Abstract() { + return new Implementation(); + } + + Abstract._internal(); +} + +class Implementation extends Abstract { + Implementation() : super._internal(); +} + +class DerivedClass4 extends DerivedClass2 {} + +class DerivedClass5 extends DerivedClass3 {} + +bool takesList(List list) => list.remove('a'); // LINT + +bool takesList2(List list) => list.remove('a'); // OK + +bool takesList3(List list) => list.remove('a'); // OK + +abstract class A implements List {} + +abstract class B extends A {} + +bool takesB(B b) => b.remove('a'); // LINT + +abstract class A1 implements List {} + +abstract class B1 extends A1 {} + +bool takesB1(B1 b) => b.remove('a'); // OK + +abstract class A3 implements List {} + +abstract class B3 extends A3 {} + +bool takesB3(B3 b) => b.remove('a'); // OK + +abstract class A2 implements List {} + +abstract class B2 extends A2 {} + +bool takesB2(B2 b) => b.remove('a'); // OK + +abstract class SomeList implements List {} + +abstract mixin class MyClass implements SomeList { + bool badMethod(String thing) => this.remove(thing); // LINT + bool badMethod1(String thing) => remove(thing); // LINT +} + +abstract class MyDerivedClass extends MyClass { + bool myConcreteBadMethod(String thing) => this.remove(thing); // LINT + bool myConcreteBadMethod1(String thing) => remove(thing); // LINT +} + +abstract class MyMixedClass extends Object with MyClass { + bool myConcreteBadMethod(String thing) => this.remove(thing); // LINT + bool myConcreteBadMethod1(String thing) => remove(thing); // LINT +} + +abstract class MyListMixedClass extends Object + with MyClass + implements List { + bool myConcreteBadMethod(String thing) => this.remove(thing); // LINT + bool myConcreteBadMethod1(String thing) => remove(thing); // LINT +} + +enum E implements List { + one, + two; + + @override + dynamic noSuchMethod(_) => throw UnsupportedError(''); + + void f() { + remove('string'); // LINT + this.remove('string'); // LINT + remove(1); // OK + } +} + +extension on List { + void f() { + remove('string'); // LINT + this.remove('string'); // LINT + remove(1); // OK + } +} diff --git a/pkg/linter/test_data/rules/literal_only_boolean_expressions.dart b/pkg/linter/test_data/rules/literal_only_boolean_expressions.dart new file mode 100644 index 00000000000..63f71544ecc --- /dev/null +++ b/pkg/linter/test_data/rules/literal_only_boolean_expressions.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N literal_only_boolean_expressions` + +// ignore_for_file: dead_code + +bool variable = true; + +void bad() { + if (!true) {} // LINT + if (true) {} // LINT + if (true && 1 != 0) {} // LINT + if (1 != 0 && true) {} // LINT + if (1 < 0 && true) {} // LINT + if (true && false) {} // LINT + if (1 != 0) {} // LINT + if (true && 1 != 0 || 3 < 4) {} // LINT + if (1 != 0 || 3 < 4 && true) {} // LINT + if (null ?? m()) {} // LINT + while(!true) {} //LINT + do {} while(false); // LINT + for ( ; true; ) { } //LINT +} + +bool m() => true; + +void bug658() { + String? text; + if ((text?.length ?? 0) != 0) {} +} diff --git a/pkg/linter/test_data/rules/no_default_cases.dart b/pkg/linter/test_data/rules/no_default_cases.dart new file mode 100644 index 00000000000..923bc793705 --- /dev/null +++ b/pkg/linter/test_data/rules/no_default_cases.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N no_default_cases` + +// Enum-like +class EL { + final int i; + const EL._(this.i); + + static const e = EL._(1); + static const f = EL._(2); + static const g = EL._(3); +} + +void el(EL e) { + switch(e) { + case EL.e : + print('e'); + break; + default : // LINT + print('default'); + } +} + +enum E { + e, f, g, +} + +void e(E e) { + switch(e) { + case E.e : + print('e'); + break; + default : // LINT + print('default'); + } +} + +void i(int i) { + switch(i) { + case 1 : + print('1'); + break; + default : // OK + print('default'); + } +} diff --git a/pkg/linter/test_data/rules/no_leading_underscores_for_library_prefixes.dart b/pkg/linter/test_data/rules/no_leading_underscores_for_library_prefixes.dart new file mode 100644 index 00000000000..043cfa238ce --- /dev/null +++ b/pkg/linter/test_data/rules/no_leading_underscores_for_library_prefixes.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N no_leading_underscores_for_library_prefixes` + +import 'dart:async' as _async; // LINT +import 'dart:convert' as _convert; // LINT +import 'dart:core' as dart_core; // OK +import 'dart:math' as dart_math; // OK diff --git a/pkg/linter/test_data/rules/no_leading_underscores_for_local_identifiers.dart b/pkg/linter/test_data/rules/no_leading_underscores_for_local_identifiers.dart new file mode 100644 index 00000000000..16e6fbfa862 --- /dev/null +++ b/pkg/linter/test_data/rules/no_leading_underscores_for_local_identifiers.dart @@ -0,0 +1,151 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N no_leading_underscores_for_local_identifiers` + +/// https://github.com/dart-lang/linter/issues/3360 +int _g() { // OK (not local) + int _bar() { // LINT + return 10; + } + var x = () { + _foo() { }; // LINT + }; + return _bar(); +} + +class _A { // OK + int _a() { // OK + int _bar() => 10; // LINT + return _bar(); + } +} + +///https://github.com/dart-lang/linter/issues/3126 +void fn0() { + for (var _ in []) { } // OK + var _ = g(); //OK + for (var __ = 0; __ < 1; ++__) { } // OK +} + +int g() => 0; + +/// https://github.com/dart-lang/linter/issues/3127 +class P { + final int _p; + const P([this._p = 7]); // OK +} + +var _foo = 0; // OK +const _foo1 = 1; // OK +final _foo2 = 2; // OK + +void fn() { + try { + } catch(_) { } // OK + var _f1, // LINT + _f2; // LINT + const _foo1 = 1; // LINT + final _foo2 = 2; // LINT + var foo_value = 0; // OK + var foo__value = 0; // OK + var foo__value_ = 0; // OK + () { + var _f = 0; // LINT + }(); +} + +void fn2(_param1) => null; // LINT + +void fn3(param) => null; // OK + +void fn4(_) => null; // OK + +void fn5(param_value) => null; // OK + +void fn6(void Function() function) { + fn6(() { + var _v = 1; // LINT + }); +} + +class TestClass { + var _foo = 0; // OK + static const _foo1 = 1; // OK + final _foo2 = 2; // OK + + void foo() { + var _foo = 0; // LINT + const _foo1 = 1; // LINT + final _foo2 = 2; // LINT + var foo_value = 0; // OK + var foo__value = 0; // OK + var foo__value_ = 0; // OK + + for(var _x in [1,2,3]) {} // LINT + for(var x in [1,2,3]) {} // OK + + [1,2,3].forEach((_x) => fn()); // LINT + [1,2,3].forEach((x) => fn()); // OK + [1,2,3].forEach((_) => fn()); // OK + + for (var _i = 0; _i < [].length; ++_i) { } // LINT + + for (var _i = 0, // LINT + _j = 0; // LINT + ; + ++_i, ++_j) {} + + try {} + catch(_error) {} // LINT + + try {} + catch(error) { // OK + } + + try {} + catch(error, _stackTrace) {} // LINT + + try {} + catch(error, stackTrace) { // OK + } + + for (var _e in ['']) { // LINT + } + + void bar(var _baz) { } // LINT + } + + void foo1(_param) {} // LINT + + void foo2(param) {} // OK + + void foo3(_) {} // OK + + void foo4(param_value) {} // OK + + void foo5(param, [_positional]) {} // LINT + + void foo6(param, [positional]) {} // OK + + // ignore: private_optional_parameter + void foo7({required _named}) {} // OK + + // ignore: private_optional_parameter + void foo8({_named}) {} // OK + + void foo9({named}) {} // OK + +} + +typedef _OutputFunction = void Function(String msg); // OK + +class C { + int _i; + C(this._i); +} + +class D extends C { + D(super._i); // OK +} diff --git a/pkg/linter/test_data/rules/no_literal_bool_comparisons.dart b/pkg/linter/test_data/rules/no_literal_bool_comparisons.dart new file mode 100644 index 00000000000..f7da4056028 --- /dev/null +++ b/pkg/linter/test_data/rules/no_literal_bool_comparisons.dart @@ -0,0 +1,34 @@ +class TestClass { + bool x = false; +} + +void foo() { + var x = false; + if (x == true) // LINT + { + print('oh'); + } + var f = true; + while (true == f) // LINT + { + print('oh'); + f = false; + } + + var c = TestClass(); + if (c.x == true) // LINT + { + print('oh'); + } + + print((x && f) != true); // LINT + print(x && (f != true)); // LINT +} + +void bar(bool x, bool? y) { + print(x == true); // LINT + print(x != true); // LINT + print(y == true); // OK + + if (x && true) print('oh'); // OK +} diff --git a/pkg/linter/test_data/rules/no_runtimeType_toString.dart b/pkg/linter/test_data/rules/no_runtimeType_toString.dart new file mode 100644 index 00000000000..a91ddd1cfb0 --- /dev/null +++ b/pkg/linter/test_data/rules/no_runtimeType_toString.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N no_runtimeType_toString` + +var o; + +class A { + var field; + String f() { + final s1 = '$runtimeType'; // LINT + final s2 = runtimeType.toString(); // LINT + final s3 = this.runtimeType.toString(); // LINT + final s4 = '${runtimeType}'; // LINT + final s5 = '${o.runtimeType}'; // OK + final s6 = o.runtimeType.toString(); // OK + final s7 = runtimeType == runtimeType; // OK + final s8 = field?.runtimeType?.toString(); // OK + try { + final s9 = '${runtimeType}'; // LINT + } catch (e) { + final s10 = '${runtimeType}'; // OK + } + final s11 = super.runtimeType.toString(); // LINT + throw '${runtimeType}'; // OK + } +} + +abstract class B { + void f() { + final s1 = '$runtimeType'; // OK + } +} + +mixin C { + void f() { + final s1 = '$runtimeType'; // OK + } +} + +class D { + void f() { + var runtimeType = 'C'; + print('$runtimeType'); // OK + } +} + +extension on A { + String f() => '$runtimeType'; // LINT +} +extension on B { + String f() => '$runtimeType'; // OK +} diff --git a/pkg/linter/test_data/rules/non_constant_identifier_names.dart b/pkg/linter/test_data/rules/non_constant_identifier_names.dart new file mode 100644 index 00000000000..cc3518147d7 --- /dev/null +++ b/pkg/linter/test_data/rules/non_constant_identifier_names.dart @@ -0,0 +1,111 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N non_constant_identifier_names` + +// ignore_for_file: unused_local_variable, unused_element, unused_field + +void main() { + var XYZ = 1; // LINT + var ABC = 1, // LINT + def = 1; + + const ZZZ = 3; // OK + + try { + for (var XYZ in []) // LINT + { + print(XYZ); + } + for (var II = 0; II < [].length; ++II) // LINT + { + print(II); + } + } on Exception catch (EE) //LINT + { + print(EE); + } on Object catch (_) //OK + {} +} + +String YO = ''; //LINT +const Z = 4; //OK + +//OK (#687): +var cell_0_0; +var cell_0_1; +var cell_1_0; +var cell_1_1; +var cell_10_11; + +// See: #2378 +class _F { + void _m() {} //OK + void _mBar() {} //OK + void _m_bar() {} //LINT +} + +void _fun() {} //OK +void _funBar() {} //OK +void _fun_bar() {} //LINT + +abstract class A { + int? _x; //OK + int? __x; //OK + int? X; //OK + static const Y = 3; // OK + + final String bar_bar; //LINT + + A(this.bar_bar); //OK + A.N(this.bar_bar); //OK + A.Named(this.bar_bar); //LINT + A._Named(this.bar_bar); //LINT + A.named_bar(this.bar_bar); //LINT + A.namedBar(this.bar_bar); //OK + A._N(this.bar_bar); //LINT + A._named(this.bar_bar); //OK + A.$Named(this.bar_bar); //OK + A._(this.bar_bar); //OK + A.__(this.bar_bar); //OK + A.___(this.bar_bar); //OK + + String foo_bar(); //LINT + + baz(var Boo); //LINT + + bar({String Name}); //LINT + + foo([String Name]); //LINT + + static Foo() => null; //LINT + + bool operator >(other); //OK + bool operator <(other); //OK + + void f() { + foo(YO); //OK + } +} + +class B { + B(String b); + factory B.Named2(String b) = B; //LINT +} + +foo() { + listen((_) {}); // OK! + listen((__) {}); // OK! + listen((_____) {}); // OK! +} + +Main() => null; //LINT + +listen(void onData(Object event)) {} + +// Generic function syntax should be OK (#805). +T scope(T Function() run) { + /* ... */ + return run(); +} diff --git a/pkg/linter/test_data/rules/null_closures.dart b/pkg/linter/test_data/rules/null_closures.dart new file mode 100644 index 00000000000..3dea1b72ca9 --- /dev/null +++ b/pkg/linter/test_data/rules/null_closures.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N null_closures` + +import 'dart:async'; +import 'dart:core'; + +void list_firstWhere() { + // firstWhere has a _named_ closure argument. + [2, 4, 6].firstWhere((e) => e.isEven, orElse: null); // LINT + [2, 4, 6].where((e) => e.isEven); // OK +} + +void iterable_singleWhere() { + // singleWhere has a _named_ closure argument. + {2, 4, 6}.singleWhere((e) => e.isEven, orElse: null); // LINT + [2, 4, 6].singleWhere((e) => e?.isEven ?? false, orElse: () => null); // OK +} + +void map_putIfAbsent() { + // putIfAbsent has a _required_ closure argument. + var map = {}; + map.putIfAbsent(7, () => null); // OK +} + +void future_wait() { + // Future.wait is a _static_ function with a _named_ argument. + Future.wait([], cleanUp: null); // LINT + Future.wait([], cleanUp: (_) => print('clean')); // OK +} + +void list_generate() { + // List.generate is a _constructor_ with a _positional_ argument. + new List.generate(3, (_) => null); // OK +} + +void map_otherMethods() { + // These methods have nothing we are concerned with. + new Map().keys; // OK + new Map().addAll({}); // OK +} diff --git a/pkg/linter/test_data/rules/omit_local_variable_types.dart b/pkg/linter/test_data/rules/omit_local_variable_types.dart new file mode 100644 index 00000000000..5c4e77ea4e7 --- /dev/null +++ b/pkg/linter/test_data/rules/omit_local_variable_types.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N omit_local_variable_types` + +// ignore_for_file: prefer_foreach, unused_local_variable + +f() { + dynamic x = 0; // OK + dynamic y = x; // OK + print(y); +} + +abstract class StringIterator implements Iterable {} + +void printItems2(StringIterator items) { + for (String item in items) { // LINT + print(item); + } +} + +void printItems2a(StringIterator items) { + for (dynamic item in items) { // OK + print(item); + } +} + +abstract class StringIterator2 implements StringIterator {} + +void printItems3(StringIterator2 items) { + for (String item in items) { // LINT + print(item); + } +} + +void printItems(Iterable items) { + for (String item in items) { // OK + print(item); + } +} + +void foo() { + final array = []; + for (a in array) { // OK + + } +} + +int goodGlobalVariable = 3, a = 4; // OK + +Map> badGroupByZip(Iterable people) { + Map> peopleByZip = >{}; // LINT + + for (Person person in people) { // LINT + peopleByZip.putIfAbsent(person.zip, () => []); + peopleByZip[person.zip]!.add(person); + } + + return peopleByZip; +} + +Map> goodGroupByZip(Iterable people) { + var peopleByZip = >{}; // OK + + for (final person in people) { // OK + peopleByZip.putIfAbsent(person.zip, () => []); + peopleByZip[person.zip]!.add(person); + } + + return peopleByZip; +} + +class Person { + int zip = 3; // OK + + Person() { + Iterable a = [], b = new Iterable.empty(); // OK + Iterable c = new Iterable.empty(), d = new Iterable.empty(); // LINT + } +} + +class LinkedListNode { + late LinkedListNode next; +} +void traverse(LinkedListNode head) { + for (LinkedListNode node = head; ; node = node.next) { // LINT + // doSomething + } +} + +void bug2708() { + const Null response = null; +} diff --git a/pkg/linter/test_data/rules/overridden_fields.dart b/pkg/linter/test_data/rules/overridden_fields.dart new file mode 100644 index 00000000000..68dded4d28f --- /dev/null +++ b/pkg/linter/test_data/rules/overridden_fields.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N overridden_fields` + +class Base { + Object field = 'lorem'; + + Object something = 'change'; +} + +mixin BaseMixin { + Object something = 'change'; +} + +class Bad1 extends Base { + final x = 1, field = 'ipsum'; // LINT +} + +class Bad2 extends Base { + @override + Object something = 'done'; // LINT +} + +class Bad3 extends Object with BaseMixin { + @override + Object something = 'done'; // LINT +} + +class Ok extends Base { + Object newField = 0; // OK + + final Object newFinal = 'ignore'; // OK +} + +class OK2 implements Base { + @override + Object something = 'done'; // OK + + @override + Object field = 0; +} + +abstract class OK3 implements Base { + @override + Object something = 'done'; // OK +} + +class GC11 extends Bad1 { + @override + Object something = 'done'; // LINT + + Object gc33 = 'gc33'; +} + +abstract class GC12 implements Bad1 { + @override + Object something = 'done'; // OK +} + +abstract class GC22 implements GC11 { + @override + Object something = 'done'; // OK +} + +class Super1 {} + +class Sub1 extends Super1 { + int y = 0; +} + +class Super2 { + int x = 0, y = 0; +} + +class Sub2 extends Super2 { + @override + int y = 0; // LINT +} + +class Super3 { + int x = 0; +} + +class Sub3 extends Super3 { + int x = 0; // LINT +} + +class A1 { + int f = 0; +} + +class B1 extends A1 {} + +abstract class C1 implements A1 {} + +class D1 extends B1 implements C1 { + @override + int f = 0; // LINT +} + +class StaticsNo { + static int a = 0; +} + +class VerifyStatic extends StaticsNo { + static int a = 0; +} + +mixin M on A1 { + @override + int f = 0; // LINT + + int g = 0; // OK +} + +abstract class BB { + abstract String s; +} + +mixin AbstractMixin { + abstract String s; +} + +class AA extends BB { + /// Overriding abstracts in NNBD is OK. + @override + String s = ''; // OK +} + +class AAA with AbstractMixin { + @override + String s = ''; // OK +} + +abstract class BBB { + abstract final String s; +} + +class AAAA extends BBB { + @override + String s = ''; // OK +} \ No newline at end of file diff --git a/pkg/linter/test_data/rules/package_api_docs.dart b/pkg/linter/test_data/rules/package_api_docs.dart new file mode 100644 index 00000000000..5448b6ca96c --- /dev/null +++ b/pkg/linter/test_data/rules/package_api_docs.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N package_api_docs` + +abstract class Foo //__LINT => TODO: fix API Model to treat tests specially +{ + /// Start a bar. + bar(); //OK + + foo() => new _Bar().baz(); //__LINT +} + +class _Bar //OK +{ + baz() => 42; //OK +} diff --git a/pkg/linter/test_data/rules/package_prefixed_library_names.dart b/pkg/linter/test_data/rules/package_prefixed_library_names.dart new file mode 100644 index 00000000000..bcd082021b8 --- /dev/null +++ b/pkg/linter/test_data/rules/package_prefixed_library_names.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N package_prefixed_library_names` + +// See: https://github.com/dart-lang/linter/issues/3395 +library linter.not_where_it_should_be; //FAILING diff --git a/pkg/linter/test_data/rules/prefer_asserts_in_initializer_lists.dart b/pkg/linter/test_data/rules/prefer_asserts_in_initializer_lists.dart new file mode 100644 index 00000000000..2df7132f2b8 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_asserts_in_initializer_lists.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_asserts_in_initializer_lists` + +get tlg => null; +tlm() => null; + +class A { + var f; + get g => null; + m() => null; + A.c1(a) { + assert(a != null); // LINT + assert(a != null); // LINT + } + // no more lint after non assert statement + A.c2(a) { + assert(a != null); // LINT + assert(a != null); // LINT + print(''); + assert(a != null); // OK + } + A.c3(a) {} // OK + // still lint after unmovable assert + A.c4(a) { + assert(a != null); // LINT + assert(a != null); // LINT + } + + // no lint if field is used + A.c6(a) { + assert(this.f != null); // OK + assert(f != null); // OK + } + // no lint if method is used + A.c7(a) { + assert(this.m() != null); // OK + assert(m() != null); // OK + } + // no lint if property access is used + A.c8(a) { + assert(this.g != null); // OK + assert(g != null); // OK + } + // lint if method call is not on current object + A.c9({f}) : f = f ?? 'f' { + assert(f != null); // LINT + assert(f.m1() != null); // LINT + assert(f.m1().m2() != null); // LINT + } + A.c10({this.f}) { + assert(f != null); // LINT + } + factory A.c11({f}) { + assert(f != null); // OK + return A.c1(1); + } + // lint for call of top level member + A.c12() { + assert(tlg != null); // LINT + assert(tlm() != null); // LINT + } + + // lint for call of static member + static get sa => null; + static sm() => null; + A.c13() { + assert(sa != null); // LINT + assert(sm() != null); // LINT + } +} + +// no lint for super class attributes +class B { + var a; + get b => null; +} + +class C extends B { + C() { + assert(a != null); // OK + assert(b != null); // OK + } +} + +// no lint for mixin attributes +mixin Mixin { + var a; +} + +class D extends Object with Mixin { + D() { + assert(a != null); // OK + } +} + +class F { + bool get foo => true; +} + +class G extends F { + G() { + assert(foo); // OK + } +} + +abstract class H { + H() { + assert(_assert()); // OK + } + + bool _assert() => true; +} diff --git a/pkg/linter/test_data/rules/prefer_asserts_with_message.dart b/pkg/linter/test_data/rules/prefer_asserts_with_message.dart new file mode 100644 index 00000000000..c2f81414755 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_asserts_with_message.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_asserts_with_message` + +m() { + assert(true); // LINT + assert(true, ''); // OK +} + +class A { + A() + : assert(true), // LINT + assert(true, ''), // OK + super(); +} diff --git a/pkg/linter/test_data/rules/prefer_collection_literals.dart b/pkg/linter/test_data/rules/prefer_collection_literals.dart new file mode 100644 index 00000000000..ba92825424d --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_collection_literals.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_collection_literals` + +import 'dart:collection'; + +//ignore_for_file: unused_local_variable +void main() { + var mapToLint = new Map(); // LINT + var LinkedHashMapToLint = new LinkedHashMap(); // LINT + + var m1 = Map.unmodifiable({}); //OK + var m2 = Map.fromIterable([]); //OK + var m3 = Map.fromIterables([], []); //OK + + var literalListInsideLiteralList = [[], []]; // OK + + var namedConstructorList = new List.filled(5, true); // OK + var namedConstructorMap = new Map.identity(); // OK + var namedConstructorLinkedHashMap = new LinkedHashMap.identity(); // OK + + var literalList = []; // OK + var literalMap = {}; // OK + + Set s = new Set(); // LINT + var s1 = new Set(); // LINT + Set s2 = new Set(); // LINT + var s3 = new Set.from(['foo', 'bar', 'baz']); // LINT + var s4 = new Set.of(['foo', 'bar', 'baz']); // LINT + + var s5 = ['foo', 'bar', 'baz'].toSet(); // LINT + + var s6 = new LinkedHashSet.from(['foo', 'bar', 'baz']); // LINT + var s7 = new LinkedHashSet.of(['foo', 'bar', 'baz']); // LINT + var s8 = new LinkedHashSet.from([]); // LINT + var s9 = new Set.from([]); // LINT + + var is1 = new Set.identity(); // OK + var is2 = new LinkedHashSet.identity(); // OK + + var ss1 = new Set(); // LINT + var ss2 = new LinkedHashSet(); // LINT + var ss3 = LinkedHashSet.from([]); // LINT + var ss4 = LinkedHashSet.of([]); // LINT + + Set ss5 = LinkedHashSet(); // LINT + LinkedHashSet ss6 = LinkedHashSet(); // OK + Object ss7 = LinkedHashSet(); // LINT + + printObject(Set()); // LINT + printSet(Set()); // LINT + printObject(LinkedHashSet()); // LINT + printSet(LinkedHashSet()); // LINT + printIndentedSet(0, LinkedHashSet()); // LINT + printHashSet(LinkedHashSet()); // OK + printIndentedHashSet(0, LinkedHashSet()); // OK + + Set ss8 = LinkedHashSet.from([1, 2, 3]); // LINT + LinkedHashSet ss9 = LinkedHashSet.from([1, 2, 3]); // OK + + Iterable iter = Iterable.empty(); // OK + var sss = Set.from(iter); // OK + + LinkedHashSet sss1 = >{}.putIfAbsent(3, () => LinkedHashSet()); // OK + + var lhs = LinkedHashSet(equals: (a, b) => false, hashCode: (o) => 13)..addAll({}); // OK + + LinkedHashMap hashMap = LinkedHashMap(); // OK + Object hashMap2 = LinkedHashMap(); // LINT + + printObject(Map()); // LINT + printMap(Map()); // LINT + printObject(LinkedHashMap()); // LINT + printMap(LinkedHashMap()); // LINT + printHashMap(LinkedHashMap()); // OK + + LinkedHashMap lhm = >{}.putIfAbsent(3, () => LinkedHashMap()); // OK +} + +void printObject(Object o) => print('$o'); +void printSet(Set ids) => print('$ids!'); +void printIndentedSet(int indent, Set ids) => print('$ids!'); +void printHashSet(LinkedHashSet ids) => printSet(ids); +void printIndentedHashSet(int indent, LinkedHashSet ids) => printSet(ids); +void printMap(Map map) => print('$map!'); +void printHashMap(LinkedHashMap map) => printMap(map); diff --git a/pkg/linter/test_data/rules/prefer_conditional_assignment.dart b/pkg/linter/test_data/rules/prefer_conditional_assignment.dart new file mode 100644 index 00000000000..ab5f5243127 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_conditional_assignment.dart @@ -0,0 +1,134 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_conditional_assignment` + +String getFullUserName(Person person) { + // Something expensive + return ''; +} + +class Person { + int x = 1; + String? _fullName; + + void badWithBlock1() { + if (_fullName == null) { // LINT + _fullName = getFullUserName(this); + } + } + + void badWithBlock2() { + if ((_fullName) == (null)) { // LINT + _fullName = getFullUserName(this); + } + } + + void badWithBlock3() { + if ((_fullName == null)) { // LINT + _fullName = getFullUserName(this); + } + } + + String? get badWithMultipleBlocks1 { + if (_fullName == null) { // LINT + { + _fullName = getFullUserName(this); + } + } + return _fullName; + } + + String? get badWithMultipleBlocks2 { + if ((_fullName) == (null)) { // LINT + { + _fullName = getFullUserName(this); + } + } + return _fullName; + } + + String? get badWithMultipleBlocks3 { + if ((_fullName == null)) { // LINT + { + _fullName = getFullUserName(this); + } + } + return _fullName; + } + + String? get badWithoutBlock1 { + if (_fullName == null) // LINT + _fullName = getFullUserName(this); + return _fullName; + } + + String? get badWithoutBlock2 { + if ((_fullName) == (null)) // LINT + _fullName = getFullUserName(this); + return _fullName; + } + + String? get badWithoutBlock3 { + if ((_fullName == null)) // LINT + _fullName = getFullUserName(this); + return _fullName; + } + + void good1() { + if (_fullName == null) { + x = 0; + } + } + + void good2() { + if (_fullName == null) { + x = 0; + } + } + + void good3() { + if (_fullName == null) { + x = 0; + _fullName = getFullUserName(this); + } + } + + void goodBecauseHasElseStatement1() { + if (_fullName == null) { // OK + _fullName = getFullUserName(this); + } else {} + } + + void goodBecauseHasElseStatement2() { + if ((_fullName) == (null)) { // OK + _fullName = getFullUserName(this); + } else {} + } + + void goodBecauseHasElseStatement3() { + if ((_fullName == null)) { // OK + _fullName = getFullUserName(this); + } else {} + } + + A a = A(); + A b = A(); + + void f() { + if (a.i == null) { // OK + b.i = 7; + } + } + + void g() { + if (a.i == null) { // LINT + a.i = 7; + } + } +} + +class A { + int? i; +} diff --git a/pkg/linter/test_data/rules/prefer_const_constructors_in_immutables.dart b/pkg/linter/test_data/rules/prefer_const_constructors_in_immutables.dart new file mode 100644 index 00000000000..320d0711c7a --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_const_constructors_in_immutables.dart @@ -0,0 +1,95 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_const_constructors_in_immutables` + +import 'package:meta/meta.dart'; + +@immutable +class A { + const A(); // OK + A.c1(); // LINT + const A.c2(); // OK + // no lint for constructor with body + A.c3() // OK + {} +} + +class B extends A { + B.c1(); // LINT + const B.c2(); // OK + // no lint for constructor with non-const super call + B.c3() : super.c1(); // OK + B.c4() : super.c2(); // LINT +} + +class C implements A { + // no lint with implements + C.c1(); // OK + const C.c2(); // OK +} + +@immutable +class D { + final _a; + Object get a => _a; + // no lint when there's a non const expression in initializer list + D.c1(a) : _a = a.toString(); // OK + D.c2(a) : _a = a; // LINT + D.c3(bool a) : _a = a && a; // LINT + D.c4(a) : _a = '${a ? a : ''}'; // LINT +} + +mixin Mixin1 {} + +class E extends A with Mixin1 { + // no lint because const leads to error : Const constructor can't be declared for a class with a mixin. + E.c1(); // OK +} + +@immutable +class F { + const factory F.fc1() = F.c1; // OK + factory F.fc2() = F.c1; // LINT + const F.c1(); +} + +@immutable +class G { + G.a(); // LINT + G.b() : this.a(); // OK + const G.c(); // OK + G.d() : this.c(); // LINT + const G.e() : this.c(); // OK +} + +@immutable +class H { + final f; + H(f) : f = f ?? f == null; // LINT +} + +int not_const = 0; + +@immutable +class HH { + final f; + HH(f) : f = not_const; // OK +} + +// no lint for class with final field initialized with new +@immutable +class I { + final f = new Object(); + I(f); // OK +} + +// no lint for factory redirected to non const constructor +@immutable +class J { + factory J.f1() = J.c1; // OK + factory J.f2() = J.c2; // LINT + J.c1() {} // OK + const J.c2(); // OK +} diff --git a/pkg/linter/test_data/rules/prefer_const_declarations.dart b/pkg/linter/test_data/rules/prefer_const_declarations.dart new file mode 100644 index 00000000000..cc5d12bace6 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_const_declarations.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_const_declarations` + +//ignore_for_file: unused_local_variable + +class C { + C(); + static C m() => C(); +} + +void constructorTearOffTests() { + final c1 = C.new; // LINT + final c2 = C.new; // LINT + final C Function() c3 = C.new; // LINT + // todo: consider cases -- https://github.com/dart-lang/linter/issues/2911 + // final c4 = C.new; // OK + // final C Function() c5 = C.new; // OK +} + +void constructorTearOffStaticTests() { + final c1 = C.m; // LINT + // todo: consider cases -- https://github.com/dart-lang/linter/issues/2911 + // final c2 = C.m; // LINT + final C Function() c3 = C.m; // LINT + final c4 = C.m; // LINT + final c5 = C.m; // OK + // final C Function() c5 = C.m; // OK -- not constant +} + +const o1 = const []; // OK +final o2 = []; // OK +final o3 = const []; // LINT +final o4 = ''; // LINT +final o5 = 1; // LINT +final o6 = 1.3; // LINT +final o7 = null; // LINT +final o8 = const {}; // LINT +final o9 = {}; // OK +final o10 = o1; // LINT + +// lint on final keyword +final a = null; // LINT + +class A { + const A(); + + static const o1 = const []; // OK + static final o2 = []; // OK + static final o3 = const []; // LINT + static final o4 = ''; // LINT + static final o5 = 1; // LINT + static final o6 = 1.3; // LINT + static final o7 = null; // LINT + static final o8 = const {}; // LINT + static final o9 = {}; // OK + + // lint on final keyword + static // + final a = null; // LINT + + final i = const []; // OK + + int m() => 0; +} + +m() { + const o1 = const []; // OK + final o2 = []; // OK + final o3 = const []; // LINT + final o4 = ''; // LINT + final o5 = 1; // LINT + final o6 = 1.3; // LINT + final o7 = null; // LINT + final o8 = const {}; // LINT + final o9 = {}; // OK + final o10 = new A(); // OK + final o11 = const A(); // LINT + final o12 = A.o1; // LINT + final o13 = A.o2; // OK + final o14 = o11.m(); // OK + + // lint on final keyword + final a = null; // LINT + + // https://github.com/dart-lang/sdk/issues/32745 + final b, c = 1; // OK + + final s = {}; // OK + final Set ids = {}; // OK + final Set ids2 = {}; // OK + + final m = {}; // OK + final Map m2 = {}; // OK + final Map m3 = {}; // OK + + final l = []; // OK + final List l2 = []; // OK + final List l3 = []; // OK +} diff --git a/pkg/linter/test_data/rules/prefer_const_literals_to_create_immutables.dart b/pkg/linter/test_data/rules/prefer_const_literals_to_create_immutables.dart new file mode 100644 index 00000000000..89a80b45e5d --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_const_literals_to_create_immutables.dart @@ -0,0 +1,84 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_const_literals_to_create_immutables` + +import 'package:meta/meta.dart'; + +@immutable +class A { + const A(a); + const A.named({a}); +} + +// basic tests +var l1 = new A([]); // LINT +var l2 = new A(const[]); // OK + +// tests for nested lists +var l3 = new A([ // LINT + []]); // LINT +var l4 = new A([ // LINT + const[]]); // OK +var l5 = new A(const[ //OK + const[]]); // OK + +// tests with maps and parenthesis +var l6 = new A({1: // LINT + []});// LINT +var l7 = new A(const {1: const []});// OK +var l8 = new A((([])));// LINT +var l9 = new A(((const[])));// OK + +// test with const inside +var l10 = new A([const A(null)]); // LINT + +// test named parameter +var l11 = new A.named(a: []); // LINT + +// test with literals +var l12 = new A([1]); // LINT +var l13 = new A([1.0]); // LINT +var l14 = new A(['']); // LINT +var l15 = new A([null]); // LINT + +// basic tests +var m1 = new A({}); // LINT +var m2 = new A(const{}); // OK + +// tests for nested maps +var m3 = new A({ // LINT + 1: {}}); // LINT +var m4 = new A({ // LINT + 1: const{}}); // OK +var m5 = new A(const{1: //OK + const{}}); // OK + +// tests with lists and parenthesis +var m6 = new A([ // LINT + {}]);// LINT +var m7 = new A(const [const {}]);// OK +var m8 = new A((({})));// LINT +var m9 = new A(((const{})));// OK + +// test with const inside +var m10 = new A({1: const A(null)}); // LINT + +// test named parameter +var m11 = new A.named(a: {}); // LINT + +// test with literals +var m12 = new A({1: 1}); // LINT +var m13 = new A({1: 1.0}); // LINT +var m14 = new A({1: ''}); // LINT +var m15 = new A({1: null}); // LINT + +// optional new +class C {} +var m16 = A([C()]); // OK + +class B { + const B(b); +} +var bb = B([1]); //OK diff --git a/pkg/linter/test_data/rules/prefer_expression_function_bodies.dart b/pkg/linter/test_data/rules/prefer_expression_function_bodies.dart new file mode 100644 index 00000000000..7d4fa153485 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_expression_function_bodies.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_expression_function_bodies` + +int bad() { // LINT + return 1; +} + +int good() { + int a = 2 + 3; + return a; +} + +class A { + int left = 1, right = 1; + bool one = false; + + int bad() { // LINT + return 1; + } + + int good() { // OK + int a = 2 + 3; + return a; + } + + int good2() { // OK + if (one) { // OK because it is not a block of a function or method + return 1; + } + return 0; + } + + get badWidth { // LINT + return right - left; + } + + int get goodWidth => right - left; // OK + + set goodWidth(int width) => right = left + width; // OK + + set goodWidth2(int width) { // OK + right = left + width; + } +} diff --git a/pkg/linter/test_data/rules/prefer_final_fields.dart b/pkg/linter/test_data/rules/prefer_final_fields.dart new file mode 100644 index 00000000000..17610727580 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_final_fields.dart @@ -0,0 +1,198 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_final_fields` + +//ignore_for_file: unused_field, unused_local_variable, prefer_expression_function_bodies + +class PrefixOps { + bool _initialized = false; // LINT + int _num = 1; // LINT + int _num2 = 1; // OK + int _bits = 0xffff; // LINT + int getValue() { + if (!_initialized) { + return 0; + } + if (-_num == -1) { + return 0; + } + if (~_bits == 0) { + return 0; + } + if (--_num2 == 0) { + return 0; + } + return 1; + } +} + +typedef bool Predicate(); + +class PostfixOps { + int _num = 1; // OK + int _num2 = 1; // OK + String _string = ''; // LINT + + Predicate _predicate = () => false; // LINT + + int getValue() { + if (_num-- == -1) { + return 0; + } + if (_num2++ == 1) { + return 0; + } + if (_predicate()) { + return 1; + } + if (_string.length == 1) { + return 0; + } + return 1; + } +} + +class FalsePositiveWhenReturn { + int _value = 0; + int getValue() { + return ++_value; // OK + } +} + +class BadImmutable { + var _label = 'hola mundo! BadImmutable'; // LINT + var label = 'hola mundo! BadImmutable'; // OK +} + +class GoodImmutable { + final label = 'hola mundo! BadImmutable', bla = 5; // OK + final _label = 'hola mundo! BadImmutable', _bla = 5; // OK +} + +class GoodMutable { + var _label = 'hola mundo! GoodMutable'; + var _someInt = 0; + var _otherInt = 1; + + void changeLabel() { + _label = 'hello world! GoodMutable'; + _someInt++; + _otherInt += 2; + } +} + +class MultipleMutable { + final int _someOther; + var _label = 'hola mundo! GoodMutable', _offender = 'mumble mumble!'; // LINT + var _never_initialized_field; // OK + + MultipleMutable.foo() : _someOther = 5; + + MultipleMutable(this._someOther); + + void changeLabel() { + _label = 'hello world! GoodMutable'; + } +} + +class BadMultipleFormals { + var _label; // LINT + BadMultipleFormals(this._label); + BadMultipleFormals.withDefault(this._label); +} + +class BadInitializer { + var _label; // LINT + BadInitializer() : _label = 'Hello'; +} + +class BadMultipleInitializer { + var _label; // LINT + BadMultipleInitializer() : _label = 'Hello'; + BadMultipleInitializer.withDefault() : _label = 'Default'; +} + +class BadMultipleMixConstructors { + var _label; // LINT + BadMultipleMixConstructors(this._label); + BadMultipleMixConstructors.withDefault() : _label = 'Hello'; +} + +class GoodFormals { + var _label; // OK + GoodFormals(this._label); + GoodFormals.empty(); +} + +class GoodInitializer { + var _label; // OK + GoodInitializer() : _label = 'Hello'; + GoodInitializer.empty(); +} + +class C { + int _f = 0; // LINT + void m() { + String _f; + _f = ''; + } +} + +class D { + int _f = 0; // OK +} + +void accessD_f() { + D d = new D(); + d._f = 42; +} + +class E { + int _f = 0; // LINT + + void useItInRightHandSide() { + // ignore: unused_local_variable + int a = _f; + } +} + +class F { + var _array = []; // LINT + + void foo() { + _array[0] = 3; + } +} + +// https://github.com/dart-lang/linter/issues/686 +class IdBug686 { + static int _id = 0; + static String generateId({prefix: String}) { + return (prefix ?? "id") + "-" + (_id++).toString() + "-" + _foo(); + } + + static String _foo() => ''; +} + +// analyzer's `FieldMember` vs `FieldElement` caused +// https://github.com/dart-lang/sdk/issues/34417 +abstract class GenericBase { + int _current = 0; // OK +} + +class GenericSub extends GenericBase { + void test() { + _current = 1; + } +} + +class Z { + /// Set in the top level [z] function below. + int _z = 1; //OK +} + +void z(Z z) { + z._z = 0; +} diff --git a/pkg/linter/test_data/rules/prefer_final_parameters.dart b/pkg/linter/test_data/rules/prefer_final_parameters.dart new file mode 100644 index 00000000000..fd2a96c4132 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_final_parameters.dart @@ -0,0 +1,155 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_final_parameters` + +void badRequiredPositional(String label) { // LINT + print(label); +} + +void goodRequiredPositional(final String label) { // OK + print(label); +} + +void badOptionalPosition([String? label]) { // LINT + print(label); +} + +void goodOptionalPosition([final String? label]) { // OK + print(label); +} + +void badRequiredNamed({required String label}) { // LINT + print(label); +} + +void goodRequiredNamed({required final String label}) { // OK + print(label); +} + +void badOptionalNamed({String? label}) { // LINT + print(label); +} + +void goodOptionalNamed({final String? label}) { // OK + print(label); +} + +void badExpression(int value) => print(value); // LINT + +void goodExpression(final int value) => print(value); // OK + +bool? _testingVariable; + +void set badSet(bool setting) => _testingVariable = setting; // LINT + +void set goodSet(final bool setting) => _testingVariable = setting; // OK + +var badClosure = (Object random) { // LINT + print(random); +}; + +var goodClosure = (final Object random) { // OK + print(random); +}; + +var _testingList = [1, 7, 15, 20]; + +void useBadClosureArgument() { + _testingList.forEach((element) => print(element + 4)); // LINT +} + +void useGoodClosureArgument() { + _testingList.forEach((final element) => print(element + 4)); // OK +} + +void useGoodTypedClosureArgument() { + _testingList.forEach((final int element) => print(element + 4)); // OK +} + +void badMixedLast(String bad, final String good) { // LINT + print(bad); + print(good); +} + +void badMixedFirst(final String goodFirst, String badSecond) { // LINT + print(goodFirst); + print(badSecond); +} + +// LINT [+1] +void badMixedMiddle(String badFirst, final String goodSecond, String badThird) { // LINT + print(badFirst); + print(goodSecond); + print(badThird); +} + +void goodMultiple(final String bad, final String good) { // OK + print(bad); + print(good); +} + +void mutableCase(String label) { // OK + print(label); + label = 'Lint away!'; + print(label); +} + +void mutableExpression(int value) => value = 3; // OK + +class C { + String value = ''; + int _contents = 0; + + C(String content) { // LINT + _contents = content.length; + } + + C.bad(int contents): _contents = contents; // LINT + + C.good(final int contents): _contents = contents; // OK + + C.badValue(String value): this.value = value; // LINT + + C.goodValue(this.value); // OK + + factory C.goodFactory(final String value) { // OK + return C(value); + } + + factory C.badFactory(String value) { // LINT + return C(value); + } + + void set badContents(int contents) => _contents = contents; // LINT + void set goodContents(final int contents) => _contents = contents; // OK + + int get contentValue => _contents + 4; // OK + + void badMethod(String bad) { // LINT + print(bad); + } + + void goodMethod(final String good) { // OK + print(good); + } + + @override + C operator +(C other) { // LINT + return C.good(contentValue + other.contentValue); + } + + @override + C operator -(final C other) { // OK + return C.good(contentValue + other.contentValue); + } +} + +class InitializingFormals { + final String initialize; + + InitializingFormals.okInitializingFormal(this.initialize); // OK + + InitializingFormals.okInitializingFormalNamed({required this.initialize}); // OK +} diff --git a/pkg/linter/test_data/rules/prefer_foreach.dart b/pkg/linter/test_data/rules/prefer_foreach.dart new file mode 100644 index 00000000000..3a628c3bbf5 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_foreach.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_foreach` + +void f(Object o) {} + +void foo() { + final myList = []; + for (final a in myList) { // LINT + f(a); + } +} + +void foo2() { + for (final a in []) { //LINT + (f(a)); + } +} + +Function func() => () {}; + +void foo3() { + for (final a in [1]) { //LINT + func()(a); + } +} + +class WithMethods { + void f(Object o) {} + + void foo() { + final myList = []; + for (final a in myList) { // LINT + f(a); + } + } +} + +class WithThirdPartyMethods { + WithMethods x = WithMethods(); + + void foo() { + final myList = []; + for (final a in myList) { // LINT + x.f(a); + } + } +} + +class WithElementInTarget { + List myList = []; + + void good() { + for (final x in myList) { // OK because x is the target + x.f(x); + } + } + + void good2() { + for (final x in myList) { // OK because x is in the target + myList[myList.indexOf(x)].f(x); + } + } +} + +class WithStaticMethods { + static void f(Object o) {} + + void foo() { + final myList = []; + for (final a in myList) { // LINT + WithStaticMethods.f(a); + } + } +} diff --git a/pkg/linter/test_data/rules/prefer_function_declarations_over_variables.dart b/pkg/linter/test_data/rules/prefer_function_declarations_over_variables.dart new file mode 100644 index 00000000000..cd87cb37ba7 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_function_declarations_over_variables.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_function_declarations_over_variables` + +void main() { + thisIsGood() { // OK + print('this is good'); + } + + var ok = () { // OK + print('this is ok'); + }; + ok = (){ + print('this is still ok'); + }; + + var bad1 = () { // LINT + print('this is bad'); + }; + + var bad2 = bad1; // OK + + bad2 = bad1; // OK + + bad2 = () { // OK + print('this is bad'); + }; + + var bad3 = print; // OK + + bad3 = print; // OK +} + +var ok2 = () {}; // OK + +final bad4 = () {}; // LINT + +class C { + var ok3 = () {}; // OK + + final bad5 = () {}; // LINT +} diff --git a/pkg/linter/test_data/rules/prefer_if_null_operators.dart b/pkg/linter/test_data/rules/prefer_if_null_operators.dart new file mode 100644 index 00000000000..2380228857a --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_if_null_operators.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_if_null_operators` + +var v; + +m(p) { + var a, b; + a == null ? b : a; // LINT + null == a ? b : a; // LINT + a != null ? a : b; // LINT + null != a ? a : b; // LINT + a.b != null ? a.b : b; // LINT + v == null ? b : v; // LINT + p == null ? b : p; // LINT +} diff --git a/pkg/linter/test_data/rules/prefer_initializing_formals.dart b/pkg/linter/test_data/rules/prefer_initializing_formals.dart new file mode 100644 index 00000000000..3c912e258db --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_initializing_formals.dart @@ -0,0 +1,261 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_initializing_formals` + +class D { + /// https://github.com/dart-lang/linter/issues/2664 + D(int initialValue) : value = initialValue; + + int? value; +} + +class C { + String? value; + C._(); + + /// https://github.com/dart-lang/linter/issues/2441 + factory C.withValue(String? value) { + var c = C._(); + c.value = value; // OK + return c; + } +} + +class Foo { + final bool isBlahEnabled; + final List whatever; + + /// https://github.com/dart-lang/linter/issues/2605 + Foo(bool isThingEnabled) + : whatever = isThingEnabled ? ['thingstuff'] : ['otherstuff'], + isBlahEnabled = isThingEnabled; // OK +} + +class A { + int x; + A(this.x); +} + +class B extends A { + B(int y) : super(y) { + x = y; // OK + } +} + +num cos(num theta) { + return 0; +} + +num sin(num theta) { + return 0; +} + +class SimpleBadCase { + num x = 0, y = 0; + SimpleBadCase(num x, num y) { + this.x = x; // LINT + this.y = y; // LINT + } +} + +class SimpleGoodCase { + num x, y; + SimpleGoodCase(this.x, this.y); +} + +class SimpleBadCaseWithOnlyOneLint { + num x, y = 0; + SimpleBadCaseWithOnlyOneLint(this.x, num y) { + this.y = y; // LINT + } +} + +/// https://github.com/dart-lang/linter/issues/2605 +class RenamedFieldsForReadability { + num x = 0, y = 0; + RenamedFieldsForReadability(num a, num b) { + x = a; // OK + y = b; // OK + } +} + +/// https://github.com/dart-lang/linter/issues/2605 +class RenamedFieldsForReadability2 { + num x = 0; + num y = 0; + RenamedFieldsForReadability2(num a, num b) { + x = a; // OK + y = b; // OK + } +} + +class NoFieldsJustSetters { + String name = ''; + NoFieldsJustSetters(num x, num y) { + this.x = x; // OK + this.y = y; // OK + } + set x(num x) { + name = 'My x value is $x'; + } + + set y(num y) { + name = 'My y value is $y'; + } +} + +class NoFieldsJustSettersWithoutThisAndWithOneGetter { + String name = ''; + NoFieldsJustSettersWithoutThisAndWithOneGetter(num a, num b) { + x = a; // OK + y = b; // OK + } + num get x { + return 0; + } + + set x(num x) { + name = 'My x value is $x'; + } + + set y(num y) { + name = 'My y value is $y'; + } +} + +class SuperCallWithConstructorParameters extends SimpleGoodCase { + num r = 0, theta = 0; + SuperCallWithConstructorParameters(num r, num theta) + : super(r * cos(theta), r * sin(theta)) { + this.r = r; // LINT + this.theta = theta; // LINT + } +} + +class NamedConstructorRenameAndInitializer { + num x, y; + NamedConstructorRenameAndInitializer.ok1(num a, this.y) + : this.x = a, // OK + super(); + NamedConstructorRenameAndInitializer.ok2(num a, this.y) + : x = a, // OK + super(); +} + +class BadCaseWithNamedConstructorAndSuperCall extends SimpleGoodCase { + num r, theta; + BadCaseWithNamedConstructorAndSuperCall.ok(num r, num theta) + : this.r = r, // LINT + this.theta = theta, // LINT + super(r * cos(theta), r * sin(theta)); +} + +class GoodCaseWithPrivateFields { + // ignore: unused_field + num? _x, _y; + GoodCaseWithPrivateFields(num x, num y) { + this._x = x; // OK // This should be lint for other rule + this._y = y; // OK // This should be lint for other rule + } +} + +class GoodCaseWithPrivateFieldsWithoutThis { + // ignore: unused_field + num _x = 0, _y = 0; + GoodCaseWithPrivateFieldsWithoutThis(num x, num y) { + _x = x; // OK // This should be lint for other rule + _y = y; // OK // This should be lint for other rule + } +} + +class GoodCaseWithPrivateFieldsInInitializers { + // ignore: unused_field + final num _x, _y; + GoodCaseWithPrivateFieldsInInitializers(num x, num y) + : this._x = x, // OK // This is the right way to do + this._y = y; // OK // This is the right way to do +} + +class GoodCaseWithPrivateFieldsInInitializersWithoutThis { + // ignore: unused_field + final num _x, _y; + GoodCaseWithPrivateFieldsInInitializersWithoutThis(num x, num y) + : _x = x, // OK // This is the right way to do + _y = y; // OK // This is the right way to do +} + +class BadCaseWithTwoFieldsOneArgument { + num x = 0, y = 0; + BadCaseWithTwoFieldsOneArgument(num x) { + this.x = x; // LINT + this.y = x; // OK + } +} + +class GoodCaseWithTwoFieldsOneArgument { + num x = 0, y = 0; + GoodCaseWithTwoFieldsOneArgument(this.x) { + y = this.x; // OK + } +} + +class GoodCaseWithTwoFieldsOneArgumentWithoutThis { + num x = 0, y = 0; + GoodCaseWithTwoFieldsOneArgumentWithoutThis(this.x) { + y = x; // OK + } +} + +class BadCaseWithOneParameterToTwoFields { + final int a, b; + + BadCaseWithOneParameterToTwoFields(int b) + : a = b, // OK + this.b = b; // LINT +} + +class GoodCaseWithOneParameterToTwoFields { + final int a, b; + + GoodCaseWithOneParameterToTwoFields(this.b) : a = b; // OK +} + +class GoodCaseWithOneParameterToTwoFieldsBecauseTheyHaveDifferentNames { + final int a, b; + + GoodCaseWithOneParameterToTwoFieldsBecauseTheyHaveDifferentNames(int c) + : a = c, // OK + b = c; // OK +} + +class BadCaseWithNamedArgs { + num? x, y; + BadCaseWithNamedArgs({num? x, num y = 1}) { + this.x = x; // LINT + this.y = y; // LINT + } +} + +class GoodCaseWithDifferentNamedArgs { + num? x, y; + GoodCaseWithDifferentNamedArgs({num? a, num b = 1}) { + this.x = a; // OK + this.y = b; // OK + } +} + +class BadCaseWithNamedArgsInitializer { + num? x, y; + BadCaseWithNamedArgsInitializer({num? x, num y = 1}) + : this.x = x, // LINT + this.y = y; // LINT +} + +class GoodCaseWithDifferentNamedArgsInitializer { + num? x, y; + GoodCaseWithDifferentNamedArgsInitializer({num? a, num b = 1}) + : this.x = a, // OK + this.y = b; // OK +} diff --git a/pkg/linter/test_data/rules/prefer_int_literals.dart b/pkg/linter/test_data/rules/prefer_int_literals.dart new file mode 100644 index 00000000000..4fe8866db4f --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_int_literals.dart @@ -0,0 +1,162 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_int_literals` + +import 'dart:math'; + +const double okDouble = 7.3; // OK +const double shouldBeInt1 = 8.0; // LINT +const double shouldBeInt2 = -8.0; // LINT +const double shouldBeInt3 = 88.00; // LINT + +// TODO(danrubel): Consider linting these as well +const double shouldBeInt4 = 8.0 + 7.0; // OK + +const inferredAsDouble = 8.0; // OK +Object inferredAsDouble2 = 8.0; // OK +dynamic inferredAsDouble3 = 8.0; // OK +final inferredAsDouble4 = 8.0 + 7.0; // OK + +class A { + var w = 7.0e2; // OK + double x = 7.0e2; // LINT + double y = 7.1e2; // LINT + double z = 7.576e2; // OK + A(this.x); + namedDouble(String s, {double d = 0.0}) {} + namedDynamic(String s, {d}) {} +} + +class B extends A { + B.one() : super(1.0); // LINT + B.two() : super(2.3); // OK + B.three() : super(3); + + namedParam1() { + namedDouble('should be int', d: 1.0); // LINT + } + + namedParam2() { + namedDynamic('should stay double', d: 1.0); // OK + } + + double typedMethodReturn1() => 6.0; // LINT + double typedMethodReturn2() { + typedMethodReturn1(); + return 6.0; // LINT + } + + untypedMethodReturn1() => 6.0; // OK + untypedMethodReturn2() { + untypedMethodReturn1(); + return 6.0; // OK + } +} + +void takesDouble(double value) {} + +void typedVar() { + takesDouble(3.0); // LINT + takesDouble(-3.0); // LINT + takesDouble(33.00); // LINT + + double myDouble1 = 5.0; // LINT + + myDouble1 = myDouble1 + 3.7; // OK + myDouble1 = 5.7 * myDouble1 / myDouble1; // OK + + // TODO(danrubel): Consider if these can be converted to an int literal + myDouble1 = 4.0 + myDouble1; // OK + myDouble1 = myDouble1 + 4.0; // OK + myDouble1 = myDouble1 - 4.0; // OK + myDouble1 = myDouble1 * 4.0; // OK + myDouble1 = myDouble1 / 4.0; // OK +} + +void untypedVar() { + var inferredAsInt = 3; + + var inferredAsDouble1 = inferredAsInt + 3.0; // OK + var inferredAsDouble2 = inferredAsDouble1 + 3.7; // OK + inferredAsDouble1 = inferredAsInt + 3.0; // OK + inferredAsDouble2 = inferredAsDouble1 + 3.7; // OK + + // No static type info is not available for these + var inferredAsDouble3 = inferredAsDouble2 + 3.0; // OK + var inferredAsDouble4 = inferredAsDouble3 - 3.0; // OK + inferredAsDouble3 = inferredAsDouble2 + 3.0; // OK + inferredAsDouble4 = inferredAsDouble3 - 3.0; // OK + + int largeInt = 1 << 61 + 1; + double largeDouble1 = largeInt * 360.0; // OK + var largeDouble2 = largeInt * 360.0; // OK + + largeDouble1 = largeDouble1 + largeDouble2 + inferredAsDouble4; +} + +double typedFunctReturn1() => 6.0; // LINT +double typedFunctReturn2(List b) { + if (b[0]) return 6.0; // LINT + if (b[1]) { + return 6.0; // LINT + } + typedFunctReturn1(); + return 6.0; // LINT +} + +untypedFunctReturn1() => 6.0; // OK +untypedFunctReturn2(List b) { + if (b[0]) return 6.0; // OK + if (b[1]) { + return 6.0; // OK + } + + double typedInnerReturn1() => 6.0; // LINT + double typedInnerReturn2() { + typedInnerReturn1(); + return 6.0; // LINT + } + + typedInnerReturn2(); + + untypedInnerReturn1() => 6.0; // OK + untypedInnerReturn2() { + untypedInnerReturn1(); + return 6.0; // OK + } + + untypedInnerReturn2(); + + untypedFunctReturn1(); + return 6.0; // OK +} + +double typedExpressionReturn() { + double myDouble = 6; + + // TODO(danrubel): Consider if this can be converted to an int literal + return 6.0 + myDouble; // OK +} + +linter_issue_1227() { + int i = 1 << 61 + 1; + var value1 = 360.0 * i; // OK + return value1; +} + +linter_issue_1231() { + double myDouble = 3; + max(3.0, 4); // OK + max(myDouble, 5.0); // LINT + var value1 = max(myDouble, 6.0); // LINT + double value2 = max(myDouble, 7.0); // LINT + return value1 + value2; +} + +class LinterIssue1230 { + static const List list1 = [50.0]; // LINT + static const list2 = [50.0]; // LINT + static const list3 = [50.0]; // OK +} diff --git a/pkg/linter/test_data/rules/prefer_is_empty.dart b/pkg/linter/test_data/rules/prefer_is_empty.dart new file mode 100644 index 00000000000..11894926184 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_is_empty.dart @@ -0,0 +1,119 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_is_empty` + +const l = ''; +const bool empty = l.length == 0; //OK + +class A { + final List a; + const A(this.a) : assert(a.length > 0); //OK +} + +class B { + final String b; + const B(this.b) : assert(b.length > 0); //OK +} + +class C { + final bool empty; + const C(dynamic l) : empty = l.length == 0; //OK +} + +class D { + final bool emptyString; + D(String s) : emptyString = s.length == 0; //LINT +} + +class E { + final bool empty; + const E(dynamic l) : empty = l.length == 0; // OK + const E.a(this.empty); + const E.b(dynamic l) : this.a(l.length == 0); // OK +} + +class F { + // ignore: avoid_positional_boolean_parameters + const F(bool b); +} + +class G extends F { + const G(dynamic l) : super(l.length == 0); // OK +} + +const int zero = 0; +Iterable list = []; +Map map = {}; + +Iterable get iterable => []; + +typedef Fun = Iterable Function(); + +Fun a() => () => []; + +bool le = list.length > 0; //LINT +bool le2 = [].length > 0; //LINT +bool le3 = ([].length as int) > 0; //LINT +bool le4 = 0 < list.length; //LINT +bool le5 = [].length < zero; +bool le6 = zero < [].length; +bool me = (map.length) == 0; //LINT +bool ie = iterable.length != 0; //LINT +bool ce = a()().length == 0; //LINT +bool mixed = list.length + map.length > 0; //OK + +Iterable length = []; +bool ok = length.first > 0; // OK + +void condition() { + final int a = list.length > 0 ? list.first : 0; //LINT + list..length; +} + +bool le7 = [].length > 1; //OK + +void testOperators() { + [].length == 0; // LINT + [].length != 0; // LINT + [].length > 0; // LINT + [].length >= 0; // LINT + [].length < 0; // LINT + [].length <= 0; // LINT + + [].length == -1; // LINT + [].length != -1; // LINT + [].length > -1; // LINT + [].length >= -1; // LINT + [].length < -1; // LINT + [].length <= -1; // LINT + + [].length == 1; // OK + [].length != 1; // OK + [].length > 1; // OK + [].length >= 1; // LINT + [].length < 1; // LINT + [].length <= 1; // OK + + 0 == [].length; // LINT + 0 != [].length; // LINT + 0 < [].length; // LINT + 0 <= [].length; // LINT + 0 > [].length; // LINT + 0 >= [].length; // LINT + + -1 == [].length; // LINT + -1 != [].length; // LINT + -1 < [].length; // LINT + -1 <= [].length; // LINT + -1 > [].length; // LINT + -1 >= [].length; // LINT + + 1 == [].length; // OK + 1 != [].length; // OK + 1 < [].length; // OK + 1 <= [].length; // LINT + 1 > [].length; // LINT + 1 >= [].length; // OK +} diff --git a/pkg/linter/test_data/rules/prefer_is_not_empty.dart b/pkg/linter/test_data/rules/prefer_is_not_empty.dart new file mode 100644 index 00000000000..888d6bdbd1f --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_is_not_empty.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_is_not_empty` + +bool lne = ![1].isEmpty; // LINT [12:12] +bool mne = !{2: 'a'}.isEmpty; // LINT +bool ine = !iterable.isEmpty; // LINT +bool parens = !([3].isEmpty); // LINT +bool parens2 = !(([4].isEmpty)); // LINT + +bool le = [].isEmpty; +bool me = {}.isEmpty; +bool ie = iterable.isEmpty; + +Iterable get iterable => []; diff --git a/pkg/linter/test_data/rules/prefer_is_not_operator.dart b/pkg/linter/test_data/rules/prefer_is_not_operator.dart new file mode 100644 index 00000000000..1e365aea2eb --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_is_not_operator.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_is_not_operator` + +class Foo {} +var _a; + +foo() { + if (!(_a is Foo)) {} // LINT + var _exp = !(_a is Foo); // LINT + + if (_a is! Foo) {} // OK + var _exp1 = _a is! Foo; // OK +} diff --git a/pkg/linter/test_data/rules/prefer_void_to_null.dart b/pkg/linter/test_data/rules/prefer_void_to_null.dart new file mode 100644 index 00000000000..1de5b25c5c6 --- /dev/null +++ b/pkg/linter/test_data/rules/prefer_void_to_null.dart @@ -0,0 +1,174 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N prefer_void_to_null` + +// TODO(mfairhurst) test void with a prefix, except that causes bugs. +// TODO(mfairhurst) test defining a class named Null (requires a 2nd file) + +// ignore_for_file: unused_local_variable + +import 'dart:async'; +import 'dart:core'; +import 'dart:core' as core; + +abstract class X { + dynamic get foo; +} + +/// https://github.com/dart-lang/linter/issues/1523 +class Y extends X { + @override + Null get foo => null; // OK +} + +/// https://github.com/dart-lang/linter/issues/2792 +class A { + Future? something() {} +} + +class B implements A { + @override + Null something() {} // OK +} + +class C { + FutureOr? something() {} +} + +class D implements C { + @override + Null something() {} // LINT +} + +class E { + Never? something() {} // LINT +} + +class F implements E { + @override + Null something() {} // OK +} + +void void_; // OK +Future? future_void; // OK +Future? future_null; // LINT +Future? future_core_null; // LINT + +void void_f() {} // OK +Null null_f() {} // LINT +core.Null core_null_f() {} // LINT +f_void(void x) {} // OK +f_null(Null x) {} // LINT +f_core_null(core.Null x) {} // LINT + +void Function(Null)? voidFunctionNull; // OK +Null Function()? nullFunctionVoid; // OK +Future Function()? FutureNullFunction; // LINT +void Function(Future)? voidFunctionFutureNull; // LINT + +usage() { + void void_; // OK + Future? future_void; // OK + Future future_null; // LINT + Future future_core_null; // LINT + + future_void?.then((_) {}); // LINT + future_void?.then((_) {}); // OK +} + +void inference() { + final _null = null; // OK + final nullReturnInferred = () {}; // OK + final nullInferred = nullReturnInferred(); // OK +} + +void emptyLiterals() { + []; // OK + [null]; // LINT + []; // OK + [null]; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {1: null}; // LINT + {"foo": null}; // LINT + {null: null}; // LINT + {null: 1}; // LINT + {null: "foo"}; // LINT + {null: null}; // LINT + {null: null}; // LINT + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {}; // OK + {1: null}; // OK + {"foo": null}; // OK + {null: null}; // OK + {null: 1}; // OK + {null: "foo"}; // OK + {null: null}; // OK + {null: null}; // OK + + // TODO(mfairhurst): is it worth handling more complex literals? +} + +variableNamedNull() { + var Null; // OK + return Null; // OK +} + +parameterNamedNull(Object Null) { + Null; // OK +} + +class AsMembers { + void void_; // OK + Null null_; // LINT + core.Null core_null; // LINT + Future? future_void; // OK + Future? future_null; // LINT + Future? future_core_null; // LINT + + void void_f() {} // OK + Null null_f() {} // LINT + core.Null core_null_f() {} // LINT + f_void(void x) {} // OK + f_null(Null x) {} // LINT + f_core_null(core.Null x) {} // LINT + + void usage() { + void void_; // OK + Future? future_void; // OK + Future future_null; // LINT + Future future_core_null; // LINT + + future_void?.then((_) {}); // LINT + future_void?.then((_) {}); // OK + } + + parameterNamedNull(Object Null) { + Null; // OK + } + + variableNamedNull() { + var Null; // OK + return Null; // OK + } +} + +class MemberNamedNull { + final Null = null; // OK +} + +extension _ on Null { // OK +} diff --git a/pkg/linter/test_data/rules/pub/package_names/_pubspec.yaml b/pkg/linter/test_data/rules/pub/package_names/_pubspec.yaml new file mode 100644 index 00000000000..027443ef3dd --- /dev/null +++ b/pkg/linter/test_data/rules/pub/package_names/_pubspec.yaml @@ -0,0 +1,4 @@ +name: FooBar #LINT +version: 0.0.1 +author: Foo +description: Foo. diff --git a/pkg/linter/test_data/rules/require_trailing_commas.dart b/pkg/linter/test_data/rules/require_trailing_commas.dart new file mode 100644 index 00000000000..8c4e344270b --- /dev/null +++ b/pkg/linter/test_data/rules/require_trailing_commas.dart @@ -0,0 +1,322 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N require_trailing_commas` + +class RequireTrailingCommasExample { + RequireTrailingCommasExample.constructor1(Object param1, Object param2); + + RequireTrailingCommasExample.constructor2( + Object param1, + Object param2, + Object param3, + ); + + RequireTrailingCommasExample.constructor3( + Object param1, Object param2, Object param3); // LINT + + RequireTrailingCommasExample.constructor4( + Object param1, + Object param2, [ + Object param3 = const [ + 'test', + ], + ]); + + RequireTrailingCommasExample.constructor5(Object param1, Object param2, + [Object param3 = const [ + 'test', + ]]); // LINT + + RequireTrailingCommasExample.constructorWithAssert1() + : assert( + true, + 'A very very very very very very very very long string', + ); + + RequireTrailingCommasExample.constructorWithAssert2() + : assert(true, + 'A very very very very very very very very long string'); // LINT + + void method1(Object p1, Object p2, {Object? param3, Object? param4}) {} + + void method2( + Object param1, + Object param2, + Object param3, + Object param4, + Object param5, + ) {} + + void method3( + Object param1, + Object param2, { + Object? param3, + Object? param4, + Object? param5, + }) {} + + void method4(Object param1, Object param2, Object param3, Object param4, + Object param5) {} // LINT + + void method5(Object param1, Object param2, + {Object? param3, Object? param4, Object? param5}) {} // LINT + + void method6(Object param1, Object param2, + {Object? param3, + Object? param4, + Object? param5, + Object? param6, + Object? param7}) {} // LINT + + void method7(Object param1, Object param2, Object param3, + {Object namedParam = true}) {} // LINT + + void run() { + void test(Object param1, Object param2, {Object? param3}) {} + + test('fits on one line, no need trailing comma', 'test'); + + test( + 'does not fit on one line, requires trailing comma', + 'test test test test test', + ); + + test('does not fit on one line, requires trailing comma', + 'test test test test test'); // LINT + + test('test', () { + // Function literal implemented using curly braces. + }); + + test('test', () { + // Function literal implemented using curly braces. + }, param3: 'test'); // LINT + + test( + 'test', + () { + // Function literal implemented using curly braces. + }, + param3: 'test', + ); + + test('test', 'test', param3: () { + // Function literal implemented using curly braces. + }); // LINT + + test( + 'test', + 'test', + param3: () { + // Function literal implemented using curly braces. + }, + ); + + test( + () { + // Function literal implemented using curly braces. + }, + 'test', + ); + + test(() { + // Function literal implemented using curly braces. + }, 'test'); // LINT + + test('map literal', { + 'one': 'test', + 'two': 'test', + }); + + test({ + 'one': 'test', + 'two': 'test', + }, 'map literal'); // LINT + + test('set literal', { + 'one', + 'two', + }); + + test({ + 'one', + 'two', + }, 'set literal'); // LINT + + test('list literal', [ + 'one', + 'two', + ]); + + test([ + 'one', + 'two', + ], 'list literal'); // LINT + + (a, b) { + // Self-executing closure. + }(1, 2); + + (one, two, three, four, five, six, seven, eight, nine, ten, + veryVeryVeryLong) //LINT + { + // Self-executing closure. + }(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 'a very very very very very very very long string'); // LINT + + test( + 'no exception for set literal as it fits entirely on 1 line', + const {'one', 'two', 'three'}, + ); + + test('no exception for set literal as it fits entirely on 1 line', + const {'one', 'two', 'three'}); // LINT + + test('exception for set literal as it spans multiple lines', const { + 'one', + 'two', + 'three', + }); + + test('exception for set literal as it spans multiple lines', const < + AnExtremelyLongClassNameOneTwoThreeFourFiveSixSevenEightNineTen>{}); + + test( + 'no exception for array literal as it fits entirely on 1 line', + const ['one', 'two', 'three'], + ); + + test('no exception for array literal as it fits entirely on 1 line', + const ['one', 'two', 'three']); // LINT + + test('exception for array literal as it spans multiple lines', const [ + 'one', + 'two', + 'three', + ]); + + test('exception for array literal as it spans multiple lines', const < + AnExtremelyLongClassNameOneTwoThreeFourFiveSixSevenEightNineTen>[]); + + test( + 'no exception for map literal as it fits entirely on 1 line', + const {'one': '1', 'two': '2', 'three': '3'}, + ); + + test('no exception for map literal as it fits entirely on 1 line', + const {'one': '1', 'two': '2', 'three': '3'}); // LINT + + test('exception for map literal as it spans multiple lines', const { + 'one': '1', + 'two': '2', + 'three': '3', + }); + + test('exception for map literal as it spans multiple lines', const {}); + + test( + 'no exception for function literal as it fits entirely on 1 line', + () {}, + ); + + test('no exception for function literal as it fits entirely on 1 line', + () {}); // LINT + + assert(true); + + assert('a very very very very very very very very very long string' + .isNotEmpty); // LINT + + assert( + 'a very very very very very very very very very long string'.isNotEmpty, + ); + + assert(false, 'a short string'); + + assert(false, + 'a very very very very very very very very very long string'); // LINT + + assert( + false, + 'a very very very very very very very very very long string', + ); + + assert(() { + return true; + }()); + + assert(() { + return true; + }(), 'comment'); // LINT + + assert( + () { + return true; + }(), + 'comment', + ); + + dynamic f; + f((a, b, c) { + return true; + }( + '', + '', + '', + )); // LINT + + f(''' +A multiline string + '''); // OK + } +} + +class AnExtremelyLongClassNameOneTwoThreeFourFiveSixSevenEightNineTen {} + +lists() { + var x; + x = []; //OK + x = [1]; //OK + x = [1, if (true) 2]; //OK + x = [ + 1, // + 2 + ]; // LINT + x = [ + 1, // + if (true) 2 + ]; // LINT +} + +sets() { + var x; + x = {}; //OK + x = {1}; //OK + x = {1, if (true) 2}; //OK + x = { + 1, // + 2 + }; // LINT + x = { + 1, // + if (true) 2 + }; // LINT +} + +maps() { + var x; + x = {}; //OK + x = {1: 1}; //OK + x = {1: 1, if (true) 2: 2}; //OK + x = { + 1: 1, // + 2: 2 + }; // LINT + x = { + 1: 1, // + if (true) 2: 2 + }; // LINT +} diff --git a/pkg/linter/test_data/rules/sized_box_shrink_expand.dart b/pkg/linter/test_data/rules/sized_box_shrink_expand.dart new file mode 100644 index 00000000000..5e09354f3f9 --- /dev/null +++ b/pkg/linter/test_data/rules/sized_box_shrink_expand.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N sized_box_shrink_expand` + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +Widget sizedBoxWithZeroWidthZeroHeight() { + return SizedBox( // LINT + height: 0, + width: 0, + child: Container(), + ); +} + +Widget sizedBoxWithInfiniteWidthInfiniteHeight() { + return SizedBox( // LINT + height: double.infinity, + width: double.infinity, + child: Container(), + ); +} + +Widget sizedBoxWithInfiniteWidthzeroHeight() { + return SizedBox( // OK + height: 0, + width: double.infinity, + child: Container(), + ); +} + +Widget sizedBoxWithZeroWidthInfiniteHeight() { + return SizedBox( // OK + height: double.infinity, + width: 0, + child: Container(), + ); +} + +Widget sizedBoxWithMixedWidthsAndHeights() { + return SizedBox( // OK + height: 26, + width: 42, + child: Container(), + ); +} + +Widget sizedBoxWithZeroWidth() { + return SizedBox( // OK + width: 0, + child: Container(), + ); +} + +Widget sizedBoxWithInfiniteWidth() { + return SizedBox( // OK + width: double.infinity, + child: Container(), + ); +} + +Widget sizedBoxWithZeroHeight() { + return SizedBox( // OK + height: 0, + child: Container(), + ); +} + +Widget sizedBoxWithInfiniteHeight() { + return SizedBox( // OK + height: double.infinity, + child: Container(), + ); +} diff --git a/pkg/linter/test_data/rules/sort_child_properties_last.dart b/pkg/linter/test_data/rules/sort_child_properties_last.dart new file mode 100644 index 00000000000..93d55656abc --- /dev/null +++ b/pkg/linter/test_data/rules/sort_child_properties_last.dart @@ -0,0 +1,117 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N sort_child_properties_last` + +// ignore_for_file: prefer_expression_function_bodies + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class W0 extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Center(), // OK + ), + ); + } +} + +class W1 extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Center(), // LINT + key: 0, + ), + ); + } +} + +class W2 extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + key: 0, + child: Center(), // OK + ), + ); + } +} + +class W3 extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + key: 0, + child: Center( + child: Column( + key: 0, + children: [], // OK + ), + ), + ), + ); + } +} + +class W4 extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + key: 0, + child: Center( + child: Column( + children: [], // LINT + key: 0, + ), + ), + ), + ); + } +} + +/// see: https://dart-review.googlesource.com/c/sdk/+/161624 +nestedChildren() { + Column( + children: [ // LINT + Column( + children: [ // LINT + Text('a'), + ], + crossAxisAlignment: CrossAxisAlignment.center, + ), + Text('b'), + Text('c'), + Text('d'), + ], + crossAxisAlignment: CrossAxisAlignment.center, + ); +} + +class WithClosure extends Widget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: RawMaterialButton( + key: Key(''), + child: Center( // OK + child: Column( + key: 0, + children: [], + ), + ), + onPressed: () { + // some code + }, + ), + ); + } +} diff --git a/pkg/linter/test_data/rules/type_init_formals.dart b/pkg/linter/test_data/rules/type_init_formals.dart new file mode 100644 index 00000000000..cff300893a0 --- /dev/null +++ b/pkg/linter/test_data/rules/type_init_formals.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N type_init_formals` + +class Good { + String name; + Good(this.name); // OK +} + +class Bad { + String name; + Bad(String this.name); //LINT [7:6] +} + +class Good2 { + num a; + Good2.i(int this.a); // OK + Good2.d(double this.a); // OK +} + +class Good3 { + var a; + Good3(int this.a); // OK +} + diff --git a/pkg/linter/test_data/rules/unawaited_futures.dart b/pkg/linter/test_data/rules/unawaited_futures.dart new file mode 100644 index 00000000000..480d5cb36e1 --- /dev/null +++ b/pkg/linter/test_data/rules/unawaited_futures.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unawaited_futures` + +import 'dart:async'; + +Future fut() => Future.value(0); + +foo1() { + fut(); +} + +foo2() async { + fut(); //LINT + + // ignore: unawaited_futures + fut(); +} + +foo3() async { + await fut(); +} + +foo4() async { + var x = fut(); + return x; +} + +foo5() async { + Duration d = Duration(); + new Future.delayed(d); //LINT +} + +foo6() async { + var map = {}; + map.putIfAbsent('foo', () => fut()); +} + +foo7() async { + _Foo() + ..doAsync() //LINT + ..doSync(); +} + +foo8() { + // Fire and forget should not be reported per existing functionality + _Foo() + ..doAsync() + ..doSync(); +} + +foo9() async { + _Foo() + ..futureField = fut(); +} + +foo10() async { + _Foo() + ..futureListField?[0] = fut(); +} + +foo11() async { + _Foo() + ..bar?.futureField = fut(); +} + +foo12() async { + final x = [fut()]; + x..[0] = fut(); +} + +foo13() async { + var y = '${fut()}'; //LINT +} + +class _Bar { + Future? futureField; +} + +class _Foo { + Future? futureField; + List>? futureListField = []; + _Bar? bar; + Future doAsync() async {} + void doSync() => null; + Future get asyncProperty => doAsync(); + List> get futures => [doAsync()]; +} + +/// https://github.com/dart-lang/linter/issues/2211 +class Future2 implements Future { + @override + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +Future2 fut2() => Future2(); + +f2() async { + fut2(); //LINT +} diff --git a/pkg/linter/test_data/rules/unmocked/unsafe_html.dart b/pkg/linter/test_data/rules/unmocked/unsafe_html.dart new file mode 100644 index 00000000000..f6ca5617fde --- /dev/null +++ b/pkg/linter/test_data/rules/unmocked/unsafe_html.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// A trimmed version of `unsafe_html` used to validate that tests can be run +/// successfully against a "real" and un-mocked SDK. +import 'dart:html'; + +void main() { + AnchorElement() + ..href = 'foo'; // LINT + var embed = EmbedElement(); + embed.src = 'foo'; // LINT + IFrameElement() + ..src = 'foo'; // LINT + + var script = ScriptElement(); + script.src = 'foo.js'; // LINT + var src = 'foo.js'; // OK + var src2 = script.src; // OK + script + ..type = 'application/javascript' + ..src = 'foo.js'; // LINT + script + ..src = 'foo.js' // LINT + ..type = 'application/javascript'; + script?.src = 'foo.js'; // LINT + + // ... +} diff --git a/pkg/linter/test_data/rules/unnecessary_brace_in_string_interps.dart b/pkg/linter/test_data/rules/unnecessary_brace_in_string_interps.dart new file mode 100644 index 00000000000..4ab4456d30a --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_brace_in_string_interps.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_brace_in_string_interps` + +main(args) { + print('hello'); + print('hello $args'); + print('hello $args!'); + print('hello ${args}1'); + print('hello ${args}'); //LINT [16:7] + print('hello ${args}!'); //LINT + print('hello ${args.length}'); + print('hello _${args}_'); + var $someString = 'Some Value'; + print('Value is: ${$someString}'); //OK +} diff --git a/pkg/linter/test_data/rules/unnecessary_lambdas.dart b/pkg/linter/test_data/rules/unnecessary_lambdas.dart new file mode 100644 index 00000000000..a13977e7171 --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_lambdas.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_lambdas` + +// ignore: avoid_annotating_with_dynamic +typedef F = int Function(dynamic s); + +void main() { + final List finalList = []; + + // Linted because parameter is final. + finalList.where((final e) => + ((a) => e.contains(a))(e)); // LINT + finalList.where((final e) => + (e.contains)(e)); // OK + finalList.where((e) => + ((a) => e.contains(a))(e)); // OK + + finalList.where((e) => // OK + ((a) => e?.contains(a) ?? false)(e)); // OK + + var deeplyNestedVariable = (a, b) { // OK + foo(foo(b)).foo(a, b); + }; +} + +foo(a) {} + +void method() { + List names = []; + names.add(names); + + var a = names.where((e) => ((e) => e.contains(e))(e)); // LINT + var b = names.where((e) => // LINT + ((e) => e?.contains(e))(e)); + names.where((e) => e?.contains(e) ?? false); // OK + + var c = names.where((e) { // LINT + return ((e) { + return e.contains(e); + })(e); + }); +} diff --git a/pkg/linter/test_data/rules/unnecessary_overrides.dart b/pkg/linter/test_data/rules/unnecessary_overrides.dart new file mode 100644 index 00000000000..68f11308615 --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_overrides.dart @@ -0,0 +1,246 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_overrides` + +import 'package:meta/meta.dart'; + +class _MyAnnotation { + const _MyAnnotation(); +} + +const _MyAnnotation myAnnotation = _MyAnnotation(); + +class Base { + int get x => 0; + set x(other) {} + + int max1(int a, int b) => 0; + int max2(int a, int b) => 0; + int m1({int a = 0, int b = 0}) => 0; + int m2({int a = 0, int b = 0}) => 0; + int m3({int a = 0, int b = 0}) => 0; + int m4({int a = 0, int b = 0}) => 0; + int operator +(other) => 0; + Base operator ~() => Base(); + @override + int get hashCode => 13; +} + +class Parent extends Base { + @override + int get x => super.x; // LINT + + @override + set x(other) // LINT + { + super.x = other; + } + + @override + int max1(int a, int b) => super.max1(a, b); // LINT + + @override + int max2(int a, int b) => super.max2(b, a); // OK + + @override + int m1({int a = 0, int b = 0}) => super.m1(a: a, b: b); // LINT + + @override + int m2({int a = 0, int b = 0}) => super.m2(b: b, a: a); // LINT + + @override + int m3({int a = 0, int b = 0}) => super.m3(b: a, a: b); // OK + + @override + int m4({int a = 0, int b = 0}) => super.m1(a: a, b: b); // OK + + @override + int operator +(other) => super + other; // LINT + + @override + Base operator ~() => ~super; // LINT + + @override + @myAnnotation + int get hashCode => super.hashCode; // OK +} + +class Okay extends Parent { + final a = new Parent(); + @override + int get x => a.x; // OK + + @override + set x(other) { // OK + a.x = other; + } +} + +class NoError extends Okay { + @override + int get x => x; // OK + + @override + set x(other) { // OK + x = other; + } +} + +class A { + void foo() {} + + void bar() {} + + int getA(Iterable a) => 0; + + int getB(Iterable a) => 0; + + int getC(Iterable a) => 0; +} + +class B extends A { + @override + void foo() // LINT + { + super.foo(); + } + + @override + void bar() { // OK + bar(); + } + + @override + int getA(Iterable a) => super.getA(a); // LINT + + @override + int getB(Iterable a) // LINT + { + return super.getB(a); + } + + @override + int getC(Iterable a) { // OK + print("something"); + return super.getC(a); + } +} + +class C { + num get g => 0; + set s(int v) {} + @protected + num m(int v) => 0; + @protected + num m1({int v = 20}) => 0; + num m2([int v = 20]) => 0; + num operator +(int other) => 0; +} +class ReturnTypeChanged extends C { + @override + int get g => super.g as int; // OK + @override + int m(int v) => super.m(v) as int; // OK + @override + int m1({int v = 20}) => super.m1(v: v) as int; // OK + @override + int m2([int v = 20]) => super.m2(v) as int; // OK + @override + int operator +(int other) => super + other as int; // OK +} +class ParameterTypeChanged extends C { + @override + set s(num v) => super.s = v as int; // OK + @override + num m(num v) => super.m(v as int); // OK + @override + num m1({num v = 20}) => super.m1(v: v as int); // OK + @override + num m2([num v = 20]) => super.m2(v as int); // OK + @override + num operator +(num other) => super.g + other; // OK +} +class ParameterNameChanged extends C { + @override + set s(num v2) => super.s = v2 as int; // OK + @override + num m(int v2) => super.m(v2); // OK + @override + num m2([int v2 = 20]) => super.m2(v2); // OK + @override + num operator +(int other2) => super + other2; // OK +} +class ParameterCovarianceChanged extends C { + @override + set s(covariant int v) => super.s = v; // OK + @override + num m(covariant int v) => super.m(v); // OK + @override + num m1({covariant int v = 20}) => super.m1(v: v); // OK + @override + num m2([covariant int v = 20]) => super.m2(v); // OK + @override + num operator +(covariant int other) => super + other; // OK +} +class ParameterAdditional extends C { + @override + num m(int v, [int v2 = 1]) => super.m(v); // OK + @override + num m1({int v = 20, int? v2}) => super.m1(v: v); // OK + @override + num m2([int v = 20, int? v2]) => super.m2(v); // OK +} +class ParameterDefaultChange extends C { + @override + num m1({int v = 10}) => super.m1(v: v); // OK + @override + num m2([int v = 10]) => super.m2(v); // OK +} +class ProtectedMadePublic extends C { + @override + num m(int v) => super.m(v); // OK + @protected + num m1({int v = 20}) => super.m1(v: v); // LINT +} + +// noSuchMethod is allowed to proxify +class D implements C { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class E extends C { + /// it's ok to override to provide better documentation + @override + num get g => super.g; // OK +} + +class F { + T m1() => throw 42; + + T get g1 => throw 42; + + void set s1(T value) => throw 42; + + T operator +(T other) => throw 42; +} + +class G extends F { + @override + int m1() => super.m1(); // LINT + + @override + int get g1 => super.g1; // LINT + + @override + void set s1(int value) => super.s1 = value; // LINT + + @override + int operator +(int other) => super + other; // LINT +} + +extension on int { + int m1() => 7; +} diff --git a/pkg/linter/test_data/rules/unnecessary_parenthesis.dart b/pkg/linter/test_data/rules/unnecessary_parenthesis.dart new file mode 100644 index 00000000000..dd8628d1bf5 --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_parenthesis.dart @@ -0,0 +1,238 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_parenthesis` + +import 'dart:async'; + +/// https://github.com/dart-lang/linter/issues/2944 +void foo() { + final items = []; + (() => [].add('something')).compose(() {}); // OK + (() => '').hashCode; // OK + (() => '').f = 1; // OK + (() => '') + 1; // OK + (() => '')[0]; // OK +} + +extension A on void Function() { + void Function() compose(void Function() other) => () { + this(); + other(); + }; + + set f(int f) {} + operator +(int x) {} + int operator [](int i) => 0; +} + +var func = (() => null); // LINT + +class D { + D.d([int? x, int? y]); +} + +/// https://github.com/dart-lang/linter/issues/2907 +void constructorTearOffs() { + var makeD = D.d; + (makeD)(1); // LINT + (D.d)(1); // LINT + (List.filled)(3, 0); // LINT + (List.filled)(3, 0); // OK + var tearoff = (List.filled); // LINT + (List).toString(); //OK +} + +var a, b, c, d; + +main() async { + 1; // OK + (1); // LINT + print(1); // OK + print((1)); // LINT + if (a && b || c && d) true; // OK + // OK because it may be hard to know all of the precedence rules. + if ((a && b) || c && d) true; // OK + (await new Future.value(1)).toString(); // OK + ('' as String).toString(); // OK + !(true as bool); // OK + (b - a) as num; // OK + (b - a) is num; // OK + a = (a); // LINT + (a) ? true : false; // LINT + true ? (a) : false; // LINT + true ? true : (a); // LINT + // OK because it is unobvious that the space-involving ternary binds tighter + // than the cascade. + (true ? [] : [])..add(''); // OK + (a ?? true) ? true : true; // OK + true ? [] : [] + ..add(''); // OK + m(p: (1 + 3)); // LINT + (a++).toString(); // OK + + // OK because it is unobvious where cascades fall in precedence. + a..b = (c..d); // OK + a.b = (c..d); // OK + a..b = (c.d); // OK + ((x) => x is bool ? x : false)(a); // OK + (fn)(a); // LINT + + // OK because unary operators mixed with space-separated tokens may have + // unexpected ordering. + !(const [7].contains(42)); // OK + !(new List.empty().contains(42)); // OK + !(await Future.value(false)); // OK + -(new List.empty().length); // OK + !(new List.empty().length.isEven); // OK + -(new List.empty().length.abs().abs().abs()); // OK + -(new List.empty().length.sign.sign.sign); // OK + !(const [7]).contains(42); // OK + + // OK because some methods are defined on Type, but removing the parentheses + // would attempt to call a _static_ method on the target. + (String).hashCode; + (int).runtimeType; + (bool).noSuchMethod(invocation()!); + (double).toString(); + + ({false: 'false', true: 'true'}).forEach((k, v) => print('$k: $v')); + ({false, true}).forEach(print); + ({false, true}).length; + ({false, true}).length.toString(); + ({1, 2, 3}) + {4}; + ({1, 2, 3}).cast; + /* comment */ ({1, 2, 3}).length; + // comment + ({1, 2, 3}).length; + print(({1, 2, 3}).length); // LINT + ([false, true]).forEach(print); // LINT + (0.sign).isEven; // LINT + (0.isEven).toString(); // LINT + (0.toString()).isEmpty; // LINT + (0.toDouble()).toString(); // LINT + + List list = []; + (list[list.length]).toString(); // LINT + + (a?.sign).hashCode; + (a?.abs()).hashCode; + (a?..abs()).hashCode; + (a?[0]).hashCode; + + (a?.sign.sign).hashCode; + (a?.abs().abs()).hashCode; + (a?..abs()..abs()).hashCode; + (a?[0][1]).hashCode; + + (a?.sign)!; + (a?.abs())!; + (a?..abs())!; + (a?[0])!; + + print(!({"a": "b"}["a"]!.isEmpty)); // LINT + + print((1 + 2)); // LINT + + print((1 == 1 ? 2 : 3)); // LINT + print('a'.substring((1 == 1 ? 2 : 3), 4)); // OK + var a1 = (1 == 1 ? 2 : 3); // OK + print('${(1 == 1 ? 2 : 3)}'); // LINT + print([(1 == 1 ? 2 : 3)]); // OK + + var a2 = (1 == 1); // OK + a2 = (1 == 1); // OK + a2 = (1 == 1) || "".isEmpty; // OK + var a3 = (1 + 1); // LINT + + // Tests for Literal and PrefixedIdentifier + var a4 = (''); // LINT + var a5 = ((a4.isEmpty), 2); // LINT + var a6 = (1, (2)); // LINT + + withManyArgs((''), false, 1); // LINT + withManyArgs('', (a4.isEmpty), 1); // LINT + withManyArgs('', (''.isEmpty), 1); // LINT + withManyArgs('', false, (1)); // LINT + + var a7 = (double.infinity).toString(); // LINT + + var list2 = ["a", null]; + var a8 = (list2.first)!.length; // LINT +} + +void withManyArgs(String a, bool b, int c) {} + +bool testTernaryAndEquality() { + if ((1 == 1 ? true : false)) // LINT + { + return (1 != 1); // OK + } else if ((1 == 1 ? true : false)) // LINT + { + return (1 > 1); // LINT + } + while ((1 == 1)) // LINT + { + print(''); + } + switch ((5 == 6)) // LINT + { + case true: + return false; + default: + return true; + } +} + +class TestConstructorFieldInitializer { + bool _x, _y; + TestConstructorFieldInitializer() + : _x = (1 == 2), // OK + _y = (true && false); // LINT +} + +int test2() => (1 == 1 ? 2 : 3); // OK +bool test3() => (1 == 1); // LINT + +Invocation? invocation() => null; + +m({p}) => null; + +bool Function(dynamic) get fn => (x) => x is bool ? x : false; + +class ClassWithFunction { + Function? f; + int? number; + + ClassWithFunction(); + ClassWithFunction.named(int a) : this.number = (a + 2); // LINT + // https://github.com/dart-lang/linter/issues/1473 + ClassWithFunction.named2(Function value) + : this.f = (value ?? (_) => 42); // OK +} + +class ClassWithClassWithFunction { + ClassWithFunction c; + + // https://github.com/dart-lang/linter/issues/1395 + ClassWithClassWithFunction() : c = (ClassWithFunction()..f = () => 42); // OK +} + +class UnnecessaryParenthesis { + ClassWithClassWithFunction c; + + UnnecessaryParenthesis() + : c = (ClassWithClassWithFunction() + ..c = (ClassWithFunction()..f = () => 42)); // OK +} + +extension on Set { + Set operator +(Set other) => {...this, ...other}; +} + +class MyType extends Type { + MyType.withString(String s) {} + MyType.withSelf(MyType myType) + : this.withString((myType.toString)()); // LINT +} diff --git a/pkg/linter/test_data/rules/unnecessary_statements.dart b/pkg/linter/test_data/rules/unnecessary_statements.dart new file mode 100644 index 00000000000..15704668935 --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_statements.dart @@ -0,0 +1,191 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_statements` + +void constructorTearOffs() { + MyClass.new; // LINT + MyClass.named; // LINT + + var m = MyClass.new; + m().foo; // LINT + m().field; // LINT +} + +String f(Object o) { + // See: https://github.com/dart-lang/linter/issues/2163 + o as int; // OK + return ''; +} + +notReturned() { + 1; // LINT + 1 + 1; // LINT + foo; // LINT + new MyClass().foo; // LINT + new MyClass()..foo; // LINT + new MyClass() + ..getter // OK + ..foo() // OK + ..foo; // LINT + []; // LINT + {}; // LINT + "blah"; // LINT + ~1; // LINT + + getter; // OK + field; // LINT + new MyClass().getter; // OK + new MyClass().field; // LINT + var myClass = new MyClass(); + myClass; // LINT + myClass.getter; // OK + myClass.field; // LINT + + new MyClass(); // OK + foo(); // OK + new MyClass().foo(); // OK + var x = 2; // OK + x++; // OK + x--; // OK + ++x; // OK + --x; // OK + try { + throw new Exception(); // OK + } catch (x) { + rethrow; // OK + } +} + +asConditionAndReturnOk() { + if (true == someBool) // OK + { + return 1 + 1; // OK + } else if (false == someBool) { + return foo; // OK + } + while (new MyClass() != null) // OK + { + return new MyClass().foo; // OK + } + while (null == someBool) // OK + { + return new MyClass()..foo; // LINT + } + for (; someBool ?? someBool;) // OK + { + return {}; // OK + } + do {} while ("blah".isEmpty); // OK + for (var i in []) {} // OK + switch (~1) // OK + { + } + + () => new MyClass().foo; // LINT + myfun() => new MyClass().foo; // OK + myfun2() => new MyClass()..foo; // LINT +} + +myfun() => new MyClass().foo; // OK +myfun2() => new MyClass()..foo; // LINT + +expressionBranching() { + null ?? 1 + 1; // LINT + null ?? foo; // LINT + null ?? new MyClass().foo; // LINT + false || 1 + 1 == 2; // LINT + false || foo == true; // LINT + false || new MyClass() as bool; // OK + false || new MyClass().foo == true; // LINT + true && 1 + 1 == 2; // LINT + true && foo == true; // LINT + true && new MyClass() as bool; // OK + true && new MyClass().foo == true; // LINT + + // ternaries can detect either/both sides + someBool // OK + ? 1 + 1 // LINT + : foo(); // OK + someBool // OK + ? foo() // OK + : foo; // LINT + someBool // OK + ? new MyClass() // OK + : foo(); // OK + someBool // OK + ? foo() // OK + : new MyClass().foo; // LINT + someBool // OK + ? [] //LINT + : {}; // LINT + + // not unnecessary condition, but unnecessary branching + foo() ?? 1 + 1; // LINT + foo() || new MyClass() as bool; // OK + foo() && foo == true; // LINT + foo() ? 1 + 1 : foo(); // LINT + foo() ? foo() : foo; // LINT + foo() ? foo() : new MyClass().foo; // LINT + + null ?? new MyClass(); // OK + null ?? foo(); // OK + null ?? new MyClass().foo(); // OK + false || foo(); // OK + false || new MyClass().foo(); // OK + true && foo(); // OK + true && new MyClass().foo(); // OK + someBool ? foo() : new MyClass().foo(); // OK + foo() ? foo() : new MyClass().foo(); // OK + foo() ? new MyClass() : foo(); // OK +} + +inOtherStatements() { + if (foo()) { + 1; // LINT + } + while (someBool) { + 1 + 1; // LINT + } + for (foo; foo();) {} // LINT + for (; foo(); 1 + 1) {} // LINT + for (; + foo(); + foo(), // OK + 1 + 1, // LINT + new MyClass().foo) {} // LINT + do { + new MyClass().foo; // LINT + } while (foo()); + + switch (foo()) { + case true: + []; // LINT + break; // OK + case false: + {}; // LINT + break; // OK + default: + "blah"; // LINT + } + + for (var i in [1, 2, 3]) { + ~1; // LINT + } +} + +bool someBool = true; +bool foo() => true; +get getter => true; +int field = 0; + +class MyClass { + int field = 0; + bool foo() => true; + + get getter => true; + + MyClass(); + MyClass.named(); +} diff --git a/pkg/linter/test_data/rules/unnecessary_this.dart b/pkg/linter/test_data/rules/unnecessary_this.dart new file mode 100644 index 00000000000..50dd7de3f6e --- /dev/null +++ b/pkg/linter/test_data/rules/unnecessary_this.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unnecessary_this` + +extension E on int { + String? f() => this?.toString(); //OK + int? get h => this?.hashCode; //OK +} + +void bar() {} + +int x = 0; + +class Parent { + int x = 0, y = 0; + void bar() {} + void foo() {} +} + +class Child extends Parent { + void SuperclassMethodWithTopLevelFunction() { + this.bar(); // OK + } + + void SuperclassMethodWithoutTopLevelFunction() { + this.foo(); // LINT + } + + void SuperclassVariableWithTopLevelVariable(int a) { + this.x = a; // OK + } + + void SuperclassVariableWithoutTopLevelVariable(int a) { + this.y = a; // LINT + } +} + +void duplicate() {} + +class A { + num x = 0, y = 0; + + A(num x, num y) { + this.x = x; // OK + this.y = y; // OK + } + + A.c1(num x, num y) + : x = x, // OK + this.y = y; // LINT + + A.bar(this.x, this.y); + + A.foo(num a, num b) { + this.x = a; // LINT + this.y = b; // LINT + this.fooMethod(); // LINT + } + + void bar2(int a) { + print(a.toString()); + } + + void barMethod() { + if (x == y) { + // ignore: unused_element + void fooMethod() { + // local function + } + this.fooMethod(); // OK + } + this.fooMethod(); // LINT + } + + void duplicate() {} + + void fooMethod() { + [].forEach((e) { + this.bar2(e); // LINT + this.duplicate(); // LINT + }); + } +} diff --git a/pkg/linter/test_data/rules/unrelated_type_equality_checks.dart b/pkg/linter/test_data/rules/unrelated_type_equality_checks.dart new file mode 100644 index 00000000000..c0dfdc51642 --- /dev/null +++ b/pkg/linter/test_data/rules/unrelated_type_equality_checks.dart @@ -0,0 +1,231 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unrelated_type_equality_checks` + +import 'package:fixnum/fixnum.dart'; + +void someFunction() { + var x = '1'; + if (x == 1) print('someFunction'); // LINT +} + +void someFunction1() { + String x = '1'; + if (x == 1) print('someFunction1'); // LINT +} + +void someFunction2() { + var x = '1'; + var y = '2'; + if (x == y) print(someFunction2); // OK +} + +void someFunction3() { + for (var i = 0; i < 10; i++) { + if (i == 0) print(someFunction3); // OK + } +} + +void someFunction4() { + var x = '1'; + if (x == null) print(someFunction4); // OK +} + +void someFunction5(Object object) { + List someList = []; + + for (ClassBase someInstance in someList) { + if (object == someInstance) print('someFunction5'); // OK + } +} + +void someFunction6(Object object) { + List someList = []; + + for (var someInstance in someList) { + if (object == someInstance) print('someFunction6'); // OK + } +} + +void someFunction7() { + List someList = []; + + if (someList.length == 0) print('someFunction7'); // OK +} + +void someFunction8(ClassBase instance) { + DerivedClass1 other = DerivedClass1(); + + if (other == instance) print('someFunction8'); // OK +} + +void someFunction9(ClassBase instance) { + var other = new DerivedClass1(); + + if (other == instance) print('someFunction9'); // OK +} + +void someFunction10(unknown) { + var what = unknown - 1; + for (var index = 0; index < unknown; index++) { + if (what == index) print('someFunction10'); // OK + } +} + +void someFunction11(Mixin instance) { + var other = new DerivedClass2(); + + if (other == instance) print('someFunction11'); // OK +} + +void someFunction12(Mixin instance) { + var other = new DerivedClass3(); + + if (other == instance) print('someFunction12'); // OK +} + +void someFunction13(DerivedClass2 instance) { + var other = new DerivedClass3(); + + if (other == instance) print('someFunction13'); // OK +} + +void someFunction14(DerivedClass4 instance) { + var other = new DerivedClass5(); + + if (other == instance) print('someFunction15'); // LINT +} + +void someFunction15() { + var x = new Int32(); + + if (x == 0) print('someFunction15'); // OK +} + +void someFunction16() { + var x = new Int32(); + + if (0 == x) print('someFunction16'); // LINT +} + +void someFunction17() { + var x = new Int64(); + + if (x == 0) print('someFunction17'); // OK +} + +void someFunction18() { + var x = new Int64(); + + if (0 == x) print('someFunction18'); // LINT +} + +void someFunction19(A a, B b) { + if (a == b) print('someFunction19'); // OK + if ([a] == [b]) print('someFunction19'); // OK + if ({'a': a} == {'b': b}) // OK + print('someFunction19'); + if ({a: 'a'} == {b: 'b'}) // OK + print('someFunction19'); +} + +void someFunction20(A a, B b) { + if (a == b) print('someFunction19'); // OK + if ([a] == [b]) print('someFunction19'); // OK + if ({'a': a} == {'b': b}) // OK + print('someFunction20'); + if ({a: 'a'} == {b: 'b'}) // OK + print('someFunction20'); +} + +void someFunction21(A a, B b) { + if (a == b) print('someFunction19'); // LINT + if ([a] == [b]) print('someFunction19'); // LINT + if ({'a': a} == {'b': b}) // LINT + print('someFunction21'); + if ({a: 'a'} == {b: 'b'}) // LINT + print('someFunction21'); +} + +void someFunction22(A a, B b) { + if (a == b) print('someFunction19'); // OK + if ([a] == [b]) print('someFunction19'); // OK + if ({'a': a} == {'b': b}) // OK + print('someFunction22'); + if ({a: 'a'} == {b: 'b'}) // OK + print('someFunction22'); +} + +void function23() { + Future getFuture() => new Future.value(); + Future? aVoidFuture; + + var aFuture = getFuture(); + if (aFuture == aVoidFuture) print('someFunction23'); // OK +} + +void function30() { + ClassWMethod c = ClassWMethod(); + if (c.determinant == 0.0) print('someFunction30'); // LINT + if (c.determinant == double) print('someFunction30'); // LINT + if (c.determinant == c.determinspider) print('someFunction30'); // OK + if (int == double) print('someFunction30'); // OK + ClassWCall callable = ClassWCall(); + if (c.determinant == callable) print('someFunction30'); // OK + SubClassWCall callable2 = SubClassWCall(); + if (c.determinant == callable2) print('someFunction30'); // OK +} + +void function31() { + var x = EnumOpKind1.delete; + if (x == EnumOpKind1.delete) print('delete'); // OK + if (x == EnumOpKind1.update) print('update'); // OK + if (x == EnumOpKind2.delete) print('delete'); // LINT + if (x == 'delete') print('delete'); // LINT +} + +void function32() { + var x = EnumImplements.a; + if (x == EnumImplements.a) print('a'); // OK + if (x == EnumImplements.b) print('b'); // OK + if (x == EnumMixin.b) print('b'); // LINT + + var y = EnumMixin.a; + if (y == EnumImplements.a) print('a'); // LINT +} + +class ClassBase {} + +class DerivedClass1 extends ClassBase {} + +mixin Mixin {} + +class DerivedClass2 extends ClassBase with Mixin {} + +class DerivedClass3 extends ClassBase implements Mixin {} + +class DerivedClass4 extends DerivedClass2 {} + +class DerivedClass5 extends DerivedClass3 {} + +class ClassWMethod { + double determinant() => 7.0; + + String determinspider() => ""; +} + +class ClassWCall { + String call() => 'hey'; +} + +class SubClassWCall extends ClassWCall {} + +enum EnumOpKind1 { insert, update, delete } + +enum EnumOpKind2 { upsert, delete } + +enum EnumImplements implements ClassBase { a, b, c } + +enum EnumMixin with Mixin { a, b, c } diff --git a/pkg/linter/test_data/rules/unsafe_html.dart b/pkg/linter/test_data/rules/unsafe_html.dart new file mode 100644 index 00000000000..b94dacb34f7 --- /dev/null +++ b/pkg/linter/test_data/rules/unsafe_html.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// test w/ `dart test -N unsafe_html` + +import 'dart:html'; + +void main() { + AnchorElement()..href = 'foo'; // LINT + var embed = EmbedElement(); + embed.src = 'foo'; // LINT + IFrameElement()..src = 'foo'; // LINT + + var script = ScriptElement(); + script.src = 'foo.js'; // LINT + var src = 'foo.js'; // OK + var src2 = script.src; // OK + script + ..type = 'application/javascript' + ..src = 'foo.js'; // LINT + script + ..src = 'foo.js' // LINT + ..type = 'application/javascript'; + script?.src = 'foo.js'; // LINT + + IFrameElement()..srcdoc = 'foo'; // LINT + + var heading = HeadingElement.h1(); + heading.createFragment('