diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml new file mode 100644 index 00000000..e0068f71 --- /dev/null +++ b/.github/workflows/flutter.yaml @@ -0,0 +1,41 @@ +name: Application ON Push & PR DO Code check +on: [ push, pull_request ] + +jobs: + code-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Check Flutter SDK version + run: flutter --version + + - name: Get dependencies + run: | + flutter pub get + flutter pub get lint_test + + - name: Check formatting + run: dart format . --set-exit-if-changed + + - name: Run default analyzer + run: flutter analyze + + - name: Run custom analyzer + run: dart run custom_lint + + - name: Run tests + run: | + # run tests if `test` folder exists + if [ -d test ] + then + flutter test -r expanded + else + echo "Tests not found." + fi diff --git a/.gitignore b/.gitignore index 65c34dc8..a273ea4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +#IDEA Files +/.idea/ + # Files and directories created by pub. .dart_tool/ .packages @@ -8,3 +11,6 @@ build/ # Omit committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock + +# Logs from custom_lint package +custom_lint.log diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/dictionaries b/.idea/dictionaries deleted file mode 100644 index 60179635..00000000 --- a/.idea/dictionaries +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba4..00000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index 627b99fe..00000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 62be7eab..00000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 639900d1..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e821027b..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b79499d9..f11b3ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.0 + +- **BREAKING CHANGE**: + - Drop support for Dart 2 while preparing for `custom_lint` support +- Add support for `cyclomatic_complexity_metric` via `custom_lint` + ## 0.0.19 - Add rules diff --git a/LICENSE b/LICENSE index bbfbde7e..60ce9323 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,28 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. + +This project also contains open source code under the following license and is explicitly marked as below: + +MIT License + +Copyright (c) 2020-2021 Dart Code Checker team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 91b2ebd8..69b2119f 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,8 @@ Then you can see suggestions in your IDE or you can run checks manually: ```bash dart analyze; -dart run dart_code_metrics:metrics analyze lib test; -dart run dart_code_metrics:metrics check-unused-files lib test; -dart run dart_code_metrics:metrics check-unused-l10n lib test; - ``` -Beware that some of the `dart_code_metrics` checks are not displayed in IDE so running checks -manually or in your actions (CI) is essential. -Learn more: https://github.com/dart-code-checker/dart-code-metrics#cli # Badge To indicate that your project is using Solid Lints, you can use the following badge: diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 21bc10aa..6ba2969f 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,21 @@ include: package:solid_lints/analysis_options.yaml + +analyzer: + plugins: + - custom_lint + +custom_lint: + rules: + - cyclomatic_complexity: + max_complexity: 4 + - number_of_parameters: + max_parameters: 2 + - function_lines_of_code: + max_lines: 50 + - avoid_non_null_assertion + - avoid_late_keyword + - avoid_global_state + - avoid_returning_widgets + - avoid_unnecessary_setstate + - double_literal_format + - avoid_unnecessary_type_assertions diff --git a/example/lib/solid_lints_example.dart b/example/lib/solid_lints_example.dart deleted file mode 100644 index 039156cf..00000000 --- a/example/lib/solid_lints_example.dart +++ /dev/null @@ -1,6 +0,0 @@ -void main() { - sum(1, 1); -} - -/// Sum of two numbers. Standard dart overflow rules apply. -int sum(int p1, int p2) => p1 + p2; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fea792a0..ad3291ca 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,10 +3,14 @@ description: A starting point for Dart libraries or applications. publish_to: none environment: - sdk: '>=2.14.4 <3.0.0' + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter dev_dependencies: + custom_lint: ^0.5.0 solid_lints: path: ../ test: ^1.20.1 - diff --git a/example/test/analysis_options.yaml b/example/test/analysis_options.yaml deleted file mode 100644 index 1f344e98..00000000 --- a/example/test/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:solid_lints/analysis_options_test.yaml diff --git a/example/test/solid_lints_example_test.dart b/example/test/solid_lints_example_test.dart deleted file mode 100644 index 1b47edad..00000000 --- a/example/test/solid_lints_example_test.dart +++ /dev/null @@ -1,368 +0,0 @@ -import 'package:solid_lints_example/solid_lints_example.dart'; -import 'package:test/test.dart'; - -class Service { - int method() { - return 0; - } -} - -class ServiceStub implements Service { - @override - int method() { - return 1; - } -} - -void main() { - //It's handy to use `late` in such situations. - late Service service; - setUp(() { - service = ServiceStub(); - }); - - //intentionally long test method to test for long-method rule - const two = 2; - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); - test("addition", () { - expect(sum(1, service.method()), equals(two)); - }); -} diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 704d89ac..74e16568 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -20,11 +20,6 @@ analyzer: # test_coverage - test/.test_coverage.dart - # Dart Code Metrics plugin (https://dartcodemetrics.dev/) provides many additional rules - # that helped to automate some pieces of our internal team code style based on the best - # industry practices. - plugins: - - dart_code_metrics language: # We've seen errors tied to use of implicit operations similar to the ones described in # https://dart.dev/guides/language/analysis-options#enabling-additional-type-checks. @@ -158,13 +153,14 @@ linter: - implementation_imports # deprecated rule in Flutter 3.7 # - invariant_booleans - - iterable_contains_unrelated_type + # - iterable_contains_unrelated_type + - collection_methods_unrelated_type - join_return_with_assignment - leading_newlines_in_multiline_strings - library_names - library_prefixes - lines_longer_than_80_chars - - list_remove_unrelated_type + # - list_remove_unrelated_type - literal_only_boolean_expressions - no_adjacent_strings_in_list - no_duplicate_case_values diff --git a/lib/lints/avoid_global_state/avoid_global_state_rule.dart b/lib/lints/avoid_global_state/avoid_global_state_rule.dart new file mode 100644 index 00000000..7ea7a2f4 --- /dev/null +++ b/lib/lints/avoid_global_state/avoid_global_state_rule.dart @@ -0,0 +1,41 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A global state rule which forbids using variables +/// that can be globally modified. +class AvoidGlobalStateRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use global state. + static const lintName = 'avoid_global_state'; + + AvoidGlobalStateRule._(super.config); + + /// Creates a new instance of [AvoidGlobalStateRule] + /// based on the lint configuration. + factory AvoidGlobalStateRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => 'Avoid variables that can be globally mutated.', + ); + + return AvoidGlobalStateRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addVariableDeclaration((node) { + final isPrivate = node.declaredElement?.isPrivate ?? false; + + if (!isPrivate && !node.isFinal && !node.isConst) { + reporter.reportErrorForNode(code, node); + } + }); + } +} diff --git a/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart new file mode 100644 index 00000000..a41fc2a2 --- /dev/null +++ b/lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -0,0 +1,71 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// A `late` keyword rule which forbids using it to avoid runtime exceptions. +class AvoidLateKeywordRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use `late` keyword. + static const lintName = 'avoid_late_keyword'; + + AvoidLateKeywordRule._(super.config); + + /// Creates a new instance of [AvoidLateKeywordRule] + /// based on the lint configuration. + factory AvoidLateKeywordRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + paramsParser: AvoidLateKeywordParameters.fromJson, + problemMessage: (_) => 'Avoid using the "late" keyword. ' + 'It may result in runtime exceptions.', + ); + + return AvoidLateKeywordRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addVariableDeclaration((node) { + if (_shouldLint(node)) { + reporter.reportErrorForNode(code, node); + } + }); + } + + bool _shouldLint(VariableDeclaration node) { + final isLateDeclaration = node.declaredElement?.isLate ?? false; + if (!isLateDeclaration) return false; + + final hasIgnoredType = _hasIgnoredType(node); + if (hasIgnoredType) return false; + + final allowInitialized = config.parameters.allowInitialized; + if (!allowInitialized) return true; + + final hasInitializer = node.initializer != null; + return !hasInitializer; + } + + bool _hasIgnoredType(VariableDeclaration node) { + final ignoredTypes = config.parameters.ignoredTypes.toSet(); + if (ignoredTypes.isEmpty) return false; + + final variableType = node.declaredElement?.type; + if (variableType == null) return false; + + final checkedTypes = [variableType, ...variableType.supertypes] + .map((t) => t.getDisplayString(withNullability: false)) + .toSet(); + + return checkedTypes.intersection(ignoredTypes).isNotEmpty; + } +} diff --git a/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart b/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart new file mode 100644 index 00000000..6051edd7 --- /dev/null +++ b/lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart @@ -0,0 +1,28 @@ +/// A data model class that represents the "avoid late keyword" input +/// parameters. +class AvoidLateKeywordParameters { + /// Allow immediately initialised late variables. + /// + /// ```dart + /// late var ok = 0; // ok when allowInitialized == true + /// late var notOk; // initialized elsewhere, not allowed + /// ``` + final bool allowInitialized; + + /// Types that would be ignored by avoid-late rule + final Iterable ignoredTypes; + + /// Constructor for [AvoidLateKeywordParameters] model + const AvoidLateKeywordParameters({ + this.allowInitialized = false, + this.ignoredTypes = const [], + }); + + /// Method for creating from json data + factory AvoidLateKeywordParameters.fromJson(Map json) => + AvoidLateKeywordParameters( + allowInitialized: json['allow_initialized'] as bool? ?? false, + ignoredTypes: + List.from(json['ignored_types'] as Iterable? ?? []), + ); +} diff --git a/lib/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart new file mode 100644 index 00000000..ec68e026 --- /dev/null +++ b/lib/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -0,0 +1,63 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// Rule which forbids using bang operator ("!") +/// as it may result in runtime exceptions. +class AvoidNonNullAssertionRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bang operator. + static const lintName = 'avoid_non_null_assertion'; + + AvoidNonNullAssertionRule._(super.config); + + /// Creates a new instance of [AvoidNonNullAssertionRule] + /// based on the lint configuration. + factory AvoidNonNullAssertionRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => 'Avoid using the bang operator. ' + 'It may result in runtime exceptions.', + ); + + return AvoidNonNullAssertionRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addPostfixExpression((node) { + if (node.operator.type != TokenType.BANG) { + return; + } + + // DCM's and Flutter's documentation treats "bang" as a valid way of + // accessing a Map. For compatibility it's excluded from this rule. + // See more: + // * https://dcm.dev/docs/rules/common/avoid-non-null-assertion + // * https://dart.dev/null-safety/understanding-null-safety#the-map-index-operator-is-nullable + final operand = node.operand; + if (operand is IndexExpression) { + final type = operand.target?.staticType; + final isInterface = type is InterfaceType; + final isMap = isInterface && + (type.isDartCoreMap || + type.allSupertypes.any((v) => v.isDartCoreMap)); + + if (isMap) { + return; + } + } + + reporter.reportErrorForNode(code, node); + }); + } +} diff --git a/lib/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart b/lib/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart new file mode 100644 index 00000000..c5e75787 --- /dev/null +++ b/lib/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart @@ -0,0 +1,50 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// A rule which forbids returning widgets from functions and methods. +class AvoidReturningWidgetsRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we return a widget. + static const lintName = 'avoid_returning_widgets'; + + AvoidReturningWidgetsRule._(super.config); + + /// Creates a new instance of [AvoidReturningWidgetsRule] + /// based on the lint configuration. + factory AvoidReturningWidgetsRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => + 'Returning a widget from a function is considered an anti-pattern. ' + 'Extract your widget to a separate class.', + ); + + return AvoidReturningWidgetsRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addDeclaration((node) { + final isWidgetReturned = switch (node) { + FunctionDeclaration(returnType: TypeAnnotation(:final type?)) || + MethodDeclaration(returnType: TypeAnnotation(:final type?)) => + hasWidgetType(type), + _ => false, + }; + + // `build` methods return widgets by nature + if (isWidgetReturned && node.declaredElement?.name != "build") { + reporter.reportErrorForNode(code, node); + } + }); + } +} diff --git a/lib/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart b/lib/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart new file mode 100644 index 00000000..d5b8329b --- /dev/null +++ b/lib/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart @@ -0,0 +1,44 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A rule which warns when setState is called inside initState, didUpdateWidget +/// or build methods and when it's called from a sync method that is called +/// inside those methods. +class AvoidUnnecessarySetStateRule extends SolidLintRule { + /// The lint name of this lint rule that represents + /// the error whether we use setState in inappropriate way. + static const lintName = 'avoid_unnecessary_setstate'; + + AvoidUnnecessarySetStateRule._(super.config); + + /// Creates a new instance of [AvoidUnnecessarySetStateRule] + /// based on the lint configuration. + factory AvoidUnnecessarySetStateRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + name: lintName, + configs: configs, + problemMessage: (_) => 'Avoid calling unnecessary setState. ' + 'Consider changing the state directly.', + ); + return AvoidUnnecessarySetStateRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + final visitor = AvoidUnnecessarySetStateVisitor(); + + context.registry.addClassDeclaration((node) { + visitor.visitClassDeclaration(node); + for (final element in visitor.setStateInvocations) { + reporter.reportErrorForNode(code, element); + } + }); + } +} diff --git a/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_method_visitor.dart b/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_method_visitor.dart new file mode 100644 index 00000000..36546be8 --- /dev/null +++ b/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_method_visitor.dart @@ -0,0 +1,62 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// AST Visitor which finds all setState invocations and checks if they are +/// necessary +class AvoidUnnecessarySetStateMethodVisitor extends RecursiveAstVisitor { + final Set _classMethodsNames; + final Iterable _bodies; + + final _setStateInvocations = []; + + /// All setState invocations + Iterable get setStateInvocations => _setStateInvocations; + + /// Constructor for AvoidUnnecessarySetStateMethodVisitor + AvoidUnnecessarySetStateMethodVisitor(this._classMethodsNames, this._bodies); + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + + final name = node.methodName.name; + final notInBody = _isNotInFunctionBody(node); + + if (name == 'setState' && notInBody) { + _setStateInvocations.add(node); + } else if (_classMethodsNames.contains(name) && + notInBody && + node.realTarget == null) { + _setStateInvocations.add(node); + } + } + + bool _isNotInFunctionBody(MethodInvocation node) => + node.thisOrAncestorMatching( + (parent) => parent is FunctionBody && !_bodies.contains(parent), + ) == + null; +} diff --git a/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_visitor.dart b/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_visitor.dart new file mode 100644 index 00000000..b9d79824 --- /dev/null +++ b/lib/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_visitor.dart @@ -0,0 +1,70 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_setstate/visitor/avoid_unnecessary_set_state_method_visitor.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// AST visitor which checks if class is State, in case yes checks its methods +class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { + static const _checkedMethods = [ + 'initState', + 'didUpdateWidget', + 'didChangeDependencies', + 'build', + ]; + + final _setStateInvocations = []; + + /// All setState invocations in checkedMethods + Iterable get setStateInvocations => _setStateInvocations; + + @override + void visitClassDeclaration(ClassDeclaration node) { + super.visitClassDeclaration(node); + + final type = node.extendsClause?.superclass.type; + if (type == null || !isWidgetStateOrSubclass(type)) { + return; + } + + final declarations = node.members.whereType().toList(); + final classMethodsNames = + declarations.map((declaration) => declaration.name.lexeme).toSet(); + final bodies = declarations.map((declaration) => declaration.body).toList(); + final methods = declarations + .where((member) => _checkedMethods.contains(member.name.lexeme)) + .toList(); + + for (final method in methods) { + final visitor = + AvoidUnnecessarySetStateMethodVisitor(classMethodsNames, bodies); + method.visitChildren(visitor); + + _setStateInvocations.addAll([ + ...visitor.setStateInvocations, + ]); + } + } +} diff --git a/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_fix.dart b/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_fix.dart new file mode 100644 index 00000000..884003df --- /dev/null +++ b/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_fix.dart @@ -0,0 +1,50 @@ +part of 'avoid_unnecessary_type_assertions_rule.dart'; + +/// A Quick fix for `avoid_unnecessary_type_assertions` rule +/// Suggests to remove unnecessary assertions +class _UnnecessaryTypeAssertionsFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addIsExpression((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + _addDeletion(reporter, operatorIsName, node, node.isOperator.offset); + } + }); + + context.registry.addMethodInvocation((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + _addDeletion( + reporter, + whereTypeMethodName, + node, + node.operator?.offset ?? node.offset, + ); + } + }); + } + + void _addDeletion( + ChangeReporter reporter, + String itemToDelete, + Expression node, + int operatorOffset, + ) { + final targetNameLength = operatorOffset - node.offset; + final removedPartLength = node.length - targetNameLength; + + final changeBuilder = reporter.createChangeBuilder( + message: "Remove unnecessary '$itemToDelete'", + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addDeletion(SourceRange(operatorOffset, removedPartLength)); + }); + } +} diff --git a/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart b/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart new file mode 100644 index 00000000..01e29339 --- /dev/null +++ b/lib/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart @@ -0,0 +1,124 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; +import 'package:solid_lints/utils/typecast_utils.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +part 'avoid_unnecessary_type_assertions_fix.dart'; + +/// The name of 'is' operator +const operatorIsName = 'is'; + +/// The name of 'whereType' method +const whereTypeMethodName = 'whereType'; + +/// An `avoid_unnecessary_type_assertions` rule which +/// warns about unnecessary usage of `is` and `whereType` operators +class AvoidUnnecessaryTypeAssertions extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const lintName = 'avoid_unnecessary_type_assertions'; + + static const _unnecessaryIsCode = LintCode( + name: lintName, + problemMessage: "Unnecessary usage of the '$operatorIsName' operator.", + ); + + static const _unnecessaryWhereTypeCode = LintCode( + name: lintName, + problemMessage: "Unnecessary usage of the '$whereTypeMethodName' method.", + ); + + AvoidUnnecessaryTypeAssertions._(super.config); + + /// Creates a new instance of [AvoidUnnecessaryTypeAssertions] + /// based on the lint configuration. + factory AvoidUnnecessaryTypeAssertions.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => "Unnecessary usage of typecast operators.", + ); + + return AvoidUnnecessaryTypeAssertions._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addIsExpression((node) { + if (_isUnnecessaryIsExpression(node)) { + reporter.reportErrorForNode(_unnecessaryIsCode, node); + } + }); + + context.registry.addMethodInvocation((node) { + if (_isUnnecessaryWhereType(node)) { + reporter.reportErrorForNode(_unnecessaryWhereTypeCode, node); + } + }); + } + + @override + List getFixes() => [_UnnecessaryTypeAssertionsFix()]; + + bool _isUnnecessaryIsExpression(IsExpression node) { + final objectType = node.expression.staticType; + final castedType = node.type.type; + + if (objectType == null || castedType == null) { + return false; + } + + final typeCast = TypeCast( + source: objectType, + target: castedType, + isReversed: true, + ); + + if (node.notOperator != null && + objectType is! TypeParameterType && + objectType is! DynamicType && + !objectType.isDartCoreObject && + typeCast.isUnnecessaryTypeCheck) { + return true; + } else { + final typeCast = TypeCast(source: objectType, target: castedType); + return typeCast.isUnnecessaryTypeCheck; + } + } + + bool _isUnnecessaryWhereType(MethodInvocation node) { + if (node + case MethodInvocation( + methodName: Identifier(name: whereTypeMethodName), + target: Expression(staticType: final targetType), + realTarget: Expression(staticType: final realTargetType), + typeArguments: TypeArgumentList(arguments: final arguments), + ) + when targetType is ParameterizedType && + isIterable(realTargetType) && + arguments.isNotEmpty) { + final objectType = targetType.typeArguments.first; + final castedType = arguments.first.type; + + if (castedType == null) { + return false; + } + + final typeCast = TypeCast(source: objectType, target: castedType); + + return typeCast.isUnnecessaryTypeCheck; + } else { + return false; + } + } +} diff --git a/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_fix.dart b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_fix.dart new file mode 100644 index 00000000..47435f50 --- /dev/null +++ b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_fix.dart @@ -0,0 +1,39 @@ +part of 'avoid_unnecessary_type_casts_rule.dart'; + +/// A Quick fix for `avoid_unnecessary_type_casts` rule +/// Suggests to remove unnecessary assertions +class _UnnecessaryTypeCastsFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addAsExpression((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + _addDeletion(reporter, 'as', node, node.asOperator.offset); + } + }); + } + + void _addDeletion( + ChangeReporter reporter, + String itemToDelete, + Expression node, + int operatorOffset, + ) { + final targetNameLength = operatorOffset - node.offset; + final removedPartLength = node.length - targetNameLength; + + final changeBuilder = reporter.createChangeBuilder( + message: "Remove unnecessary '$itemToDelete'", + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addDeletion(SourceRange(operatorOffset, removedPartLength)); + }); + } +} diff --git a/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart new file mode 100644 index 00000000..5845e87e --- /dev/null +++ b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart @@ -0,0 +1,51 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +part 'avoid_unnecessary_type_casts_fix.dart'; + +/// An `avoid_unnecessary_type_casts` rule which +/// warns about unnecessary usage of `as` operator +class AvoidUnnecessaryTypeCastsRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const lintName = 'avoid_unnecessary_type_casts'; + + AvoidUnnecessaryTypeCastsRule._(super.config); + + /// Creates a new instance of [AvoidUnnecessaryTypeCastsRule] + /// based on the lint configuration. + factory AvoidUnnecessaryTypeCastsRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => "Avoid unnecessary usage of as operator.", + ); + + return AvoidUnnecessaryTypeCastsRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addAsExpression((node) { + final visitor = AvoidUnnecessaryTypeCastsVisitor(); + visitor.visitAsExpression(node); + + for (final element in visitor.expressions.entries) { + reporter.reportErrorForNode(code, element.key); + } + }); + } + + @override + List getFixes() => [_UnnecessaryTypeCastsFix()]; +} diff --git a/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_visitor.dart b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_visitor.dart new file mode 100644 index 00000000..69d01579 --- /dev/null +++ b/lib/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_visitor.dart @@ -0,0 +1,56 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/utils/typecast_utils.dart'; + +/// AST Visitor which finds all as expressions and checks if they are +/// necessary +class AvoidUnnecessaryTypeCastsVisitor extends RecursiveAstVisitor { + final _expressions = {}; + + /// All as expressions + Map get expressions => _expressions; + + @override + void visitAsExpression(AsExpression node) { + super.visitAsExpression(node); + + final objectType = node.expression.staticType; + final castedType = node.type.type; + + if (objectType == null || castedType == null) { + return; + } + + final typeCast = TypeCast( + source: objectType, + target: castedType, + ); + + if (typeCast.isUnnecessaryTypeCheck) { + _expressions[node] = 'as'; + } + } +} diff --git a/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart b/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart new file mode 100644 index 00000000..09d17505 --- /dev/null +++ b/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `avoid_unrelated_type_assertions` rule which +/// warns about unnecessary usage of `as` operator +class AvoidUnrelatedTypeAssertionsRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const lintName = 'avoid_unrelated_type_assertions'; + + AvoidUnrelatedTypeAssertionsRule._(super.config); + + /// Creates a new instance of [AvoidUnrelatedTypeAssertionsRule] + /// based on the lint configuration. + factory AvoidUnrelatedTypeAssertionsRule.createRule( + CustomLintConfigs configs, + ) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => + 'Avoid unrelated "is" assertion. The result is always "false".', + ); + + return AvoidUnrelatedTypeAssertionsRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addIsExpression((node) { + final visitor = AvoidUnrelatedTypeAssertionsVisitor(); + visitor.visitIsExpression(node); + + for (final element in visitor.expressions.entries) { + reporter.reportErrorForNode(code, element.key); + } + }); + } +} diff --git a/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_visitor.dart b/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_visitor.dart new file mode 100644 index 00000000..55430aa2 --- /dev/null +++ b/lib/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_visitor.dart @@ -0,0 +1,149 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +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'; + +/// AST Visitor which finds all is expressions and checks if they are +/// unrelated (result always false) +class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { + final _expressions = {}; + + /// All is expressions + Map get expressions => _expressions; + + @override + void visitIsExpression(IsExpression node) { + super.visitIsExpression(node); + + final castedType = node.type.type; + if (node.notOperator != null || castedType is TypeParameterType) { + return; + } + + final objectType = node.expression.staticType; + + if (_isUnrelatedTypeCheck(objectType, castedType)) { + _expressions[node] = + '${node.isOperator.keyword?.lexeme ?? ''}${node.notOperator ?? ''}'; + } + } + + bool _isUnrelatedTypeCheck(DartType? objectType, DartType? castedType) { + if (objectType == null || castedType == null) { + return false; + } + + if (objectType is DynamicType || castedType is DynamicType) { + return false; + } + + if (objectType is! ParameterizedType || castedType is! ParameterizedType) { + return false; + } + + final objectCastedType = + _foundCastedTypeInObjectTypeHierarchy(objectType, castedType); + final castedObjectType = + _foundCastedTypeInObjectTypeHierarchy(castedType, objectType); + if (objectCastedType == null && castedObjectType == null) { + return true; + } + + if (objectCastedType == null || castedObjectType == null) { + return false; + } + + if (_checkGenerics(objectCastedType, castedType) && + _checkGenerics(castedObjectType, objectType)) { + return true; + } + + return false; + } + + DartType? _foundCastedTypeInObjectTypeHierarchy( + DartType objectType, + DartType castedType, + ) { + if (_isFutureOrAndFuture(objectType, castedType)) { + return objectType; + } + + final correctObjectType = + objectType is InterfaceType && objectType.isDartAsyncFutureOr + ? objectType.typeArguments.first + : objectType; + + if ((correctObjectType.element == castedType.element) || + castedType is DynamicType || + correctObjectType is DynamicType || + _isObjectAndEnum(correctObjectType, castedType)) { + return correctObjectType; + } + + if (correctObjectType is InterfaceType) { + return correctObjectType.allSupertypes + .firstWhereOrNull((value) => value.element == castedType.element); + } + + return null; + } + + bool _checkGenerics(DartType objectType, DartType castedType) { + if (objectType is DynamicType || castedType is DynamicType) { + return false; + } + + if (objectType is! ParameterizedType || castedType is! ParameterizedType) { + return false; + } + + final length = objectType.typeArguments.length; + if (length != castedType.typeArguments.length) { + return false; + } + + for (var argumentIndex = 0; argumentIndex < length; argumentIndex++) { + final objectGenericType = objectType.typeArguments[argumentIndex]; + final castedGenericType = castedType.typeArguments[argumentIndex]; + + if (_isUnrelatedTypeCheck(objectGenericType, castedGenericType) && + _isUnrelatedTypeCheck(castedGenericType, objectGenericType)) { + return true; + } + } + + return false; + } + + bool _isFutureOrAndFuture(DartType objectType, DartType castedType) => + objectType.isDartAsyncFutureOr && castedType.isDartAsyncFuture; + + bool _isObjectAndEnum(DartType objectType, DartType castedType) => + objectType.isDartCoreObject && + castedType.element?.kind == ElementKind.ENUM; +} diff --git a/lib/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart b/lib/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart new file mode 100644 index 00000000..c2338411 --- /dev/null +++ b/lib/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart @@ -0,0 +1,45 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_unused_parameters/avoid_unused_parameters_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `avoid_unused_parameters` rule which +/// warns about unused parameters +class AvoidUnusedParametersRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const String lintName = 'avoid_unused_parameters'; + + AvoidUnusedParametersRule._(super.config); + + /// Creates a new instance of [AvoidUnusedParametersRule] + /// based on the lint configuration. + factory AvoidUnusedParametersRule.createRule( + CustomLintConfigs configs, + ) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => 'Parameter is unused.', + ); + + return AvoidUnusedParametersRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = AvoidUnusedParametersVisitor(); + node.accept(visitor); + + for (final element in visitor.unusedParameters) { + reporter.reportErrorForNode(code, element); + } + }); + } +} diff --git a/lib/lints/avoid_unused_parameters/avoid_unused_parameters_visitor.dart b/lib/lints/avoid_unused_parameters/avoid_unused_parameters_visitor.dart new file mode 100644 index 00000000..7283cad2 --- /dev/null +++ b/lib/lints/avoid_unused_parameters/avoid_unused_parameters_visitor.dart @@ -0,0 +1,193 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +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 'package:solid_lints/utils/node_utils.dart'; +import 'package:solid_lints/utils/parameter_utils.dart'; + +/// AST Visitor which finds all is expressions and checks if they are +/// unrelated (result always false) +class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { + final _unusedParameters = []; + + /// List of unused parameters + Iterable get unusedParameters => _unusedParameters; + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + super.visitConstructorDeclaration(node); + + final parent = node.parent; + final parameters = node.parameters; + + if (parent is ClassDeclaration && parent.abstractKeyword != null || + node.externalKeyword != null || + parameters.parameters.isEmpty) { + return; + } + + _unusedParameters.addAll( + _getUnusedParameters( + node.body, + parameters.parameters, + initializers: node.initializers, + ).whereNot(nameConsistsOfUnderscoresOnly), + ); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + super.visitMethodDeclaration(node); + + final parent = node.parent; + final parameters = node.parameters; + + if (parent is ClassDeclaration && parent.abstractKeyword != null || + node.isAbstract || + node.externalKeyword != null || + (parameters == null || parameters.parameters.isEmpty)) { + return; + } + + final isTearOff = _usedAsTearOff(node); + + if (!isOverride(node.metadata) && !isTearOff) { + _unusedParameters.addAll( + _getUnusedParameters( + node.body, + parameters.parameters, + ).whereNot(nameConsistsOfUnderscoresOnly), + ); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + super.visitFunctionDeclaration(node); + + final parameters = node.functionExpression.parameters; + + if (node.externalKeyword != null || + (parameters == null || parameters.parameters.isEmpty)) { + return; + } + + _unusedParameters.addAll( + _getUnusedParameters( + node.functionExpression.body, + parameters.parameters, + ).whereNot(nameConsistsOfUnderscoresOnly), + ); + } + + Set _getUnusedParameters( + AstNode body, + Iterable parameters, { + NodeList? initializers, + }) { + final result = {}; + final visitor = _IdentifiersVisitor(); + body.visitChildren(visitor); + initializers?.accept(visitor); + + final allIdentifierElements = visitor.elements; + + for (final parameter in parameters) { + final name = parameter.name; + final isPresentInAll = allIdentifierElements.contains( + parameter.declaredElement, + ); + + /// Variables declared and initialized as 'Foo(this.param)' + bool isFieldFormalParameter = parameter is FieldFormalParameter; + + /// Variables declared and initialized as 'Foo(super.param)' + bool isSuperFormalParameter = parameter is SuperFormalParameter; + + if (parameter is DefaultFormalParameter) { + /// Variables as 'Foo({super.param})' or 'Foo({this.param})' + /// is being reported as [DefaultFormalParameter] instead + /// of [SuperFormalParameter] it seems to be an issue in DartSDK + isFieldFormalParameter = parameter.toSource().contains('this.'); + isSuperFormalParameter = parameter.toSource().contains('super.'); + } + + if (name != null && + !isPresentInAll && + !isFieldFormalParameter && + !isSuperFormalParameter) { + result.add(parameter); + } + } + + return result; + } + + bool _usedAsTearOff(MethodDeclaration node) { + final name = node.name.lexeme; + if (!Identifier.isPrivateName(name)) { + return false; + } + + final visitor = _InvocationsVisitor(name); + node.root.visitChildren(visitor); + + return visitor.hasTearOffInvocations; + } +} + +class _IdentifiersVisitor extends RecursiveAstVisitor { + final elements = {}; + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + super.visitSimpleIdentifier(node); + + final element = node.staticElement; + if (element != null) { + elements.add(element); + } + } +} + +class _InvocationsVisitor extends RecursiveAstVisitor { + final String methodName; + + bool hasTearOffInvocations = false; + + _InvocationsVisitor(this.methodName); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (node.name == methodName && + node.staticElement is MethodElement && + node.parent is ArgumentList) { + hasTearOffInvocations = true; + } + + super.visitSimpleIdentifier(node); + } +} diff --git a/lib/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart b/lib/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart new file mode 100644 index 00000000..0e339886 --- /dev/null +++ b/lib/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart @@ -0,0 +1,49 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; +import 'package:solid_lints/lints/cyclomatic_complexity/visitor/cyclomatic_complexity_flow_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A Complexity metric checks content of block and detects more easier solution +class CyclomaticComplexityMetric + extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if complexity + /// reaches maximum value. + static const lintName = 'cyclomatic_complexity'; + + CyclomaticComplexityMetric._(super.rule); + + /// Creates a new instance of [CyclomaticComplexityMetric] + /// based on the lint configuration. + factory CyclomaticComplexityMetric.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + paramsParser: CyclomaticComplexityParameters.fromJson, + problemMessage: (value) => + 'The maximum allowed complexity of a function is ' + '${value.maxComplexity}. Please decrease it.', + ); + + return CyclomaticComplexityMetric._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + final visitor = CyclomaticComplexityFlowVisitor(); + + context.registry.addBlockFunctionBody((node) { + node.visitChildren(visitor); + + if (visitor.complexityEntities.length + 1 > + config.parameters.maxComplexity) { + reporter.reportErrorForNode(code, node); + } + }); + } +} diff --git a/lib/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart b/lib/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart new file mode 100644 index 00000000..e10cc09e --- /dev/null +++ b/lib/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart @@ -0,0 +1,19 @@ +/// A data model class that represents the cyclomatic complexity input +/// paramters. +class CyclomaticComplexityParameters { + /// Min value of complexity level + final int maxComplexity; + + static const _defaultMaxComplexity = 2; + + /// Constructor for [CyclomaticComplexityParameters] model + const CyclomaticComplexityParameters({ + required this.maxComplexity, + }); + + /// Method for creating from json data + factory CyclomaticComplexityParameters.fromJson(Map json) => + CyclomaticComplexityParameters( + maxComplexity: json['max_complexity'] as int? ?? _defaultMaxComplexity, + ); +} diff --git a/lib/lints/cyclomatic_complexity/visitor/cyclomatic_complexity_flow_visitor.dart b/lib/lints/cyclomatic_complexity/visitor/cyclomatic_complexity_flow_visitor.dart new file mode 100644 index 00000000..c101ced4 --- /dev/null +++ b/lib/lints/cyclomatic_complexity/visitor/cyclomatic_complexity_flow_visitor.dart @@ -0,0 +1,144 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// ignore_for_file: public_member_api_docs + +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/ast/visitor.dart'; + +/// The AST visitor that will collect cyclomatic complexity of visit nodes in an +/// AST structure. +class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { + static const _complexityTokenTypes = [ + TokenType.AMPERSAND_AMPERSAND, + TokenType.BAR_BAR, + TokenType.QUESTION_PERIOD, + TokenType.QUESTION_QUESTION, + TokenType.QUESTION_QUESTION_EQ, + ]; + + final _complexityEntities = {}; + + /// Returns an array of entities that increase cyclomatic complexity. + Iterable get complexityEntities => _complexityEntities; + + @override + void visitAssertStatement(AssertStatement node) { + _increaseComplexity(node); + + super.visitAssertStatement(node); + } + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + _visitBlock( + node.block.leftBracket.next, + node.block.rightBracket, + ); + + super.visitBlockFunctionBody(node); + } + + @override + void visitCatchClause(CatchClause node) { + _increaseComplexity(node); + + super.visitCatchClause(node); + } + + @override + void visitConditionalExpression(ConditionalExpression node) { + _increaseComplexity(node); + + super.visitConditionalExpression(node); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _visitBlock( + node.expression.beginToken.previous, + node.expression.endToken.next, + ); + + super.visitExpressionFunctionBody(node); + } + + @override + void visitForStatement(ForStatement node) { + _increaseComplexity(node); + + super.visitForStatement(node); + } + + @override + void visitIfStatement(IfStatement node) { + _increaseComplexity(node); + + super.visitIfStatement(node); + } + + @override + void visitSwitchCase(SwitchCase node) { + _increaseComplexity(node); + + super.visitSwitchCase(node); + } + + @override + void visitSwitchDefault(SwitchDefault node) { + _increaseComplexity(node); + + super.visitSwitchDefault(node); + } + + @override + void visitWhileStatement(WhileStatement node) { + _increaseComplexity(node); + + super.visitWhileStatement(node); + } + + @override + void visitYieldStatement(YieldStatement node) { + _increaseComplexity(node); + + super.visitYieldStatement(node); + } + + void _visitBlock(Token? firstToken, Token? lastToken) { + var token = firstToken; + while (token != lastToken && token != null) { + if (token.matchesAny(_complexityTokenTypes)) { + _increaseComplexity(token); + } + + token = token.next; + } + } + + void _increaseComplexity(SyntacticEntity entity) { + _complexityEntities.add(entity); + } +} diff --git a/lib/lints/double_literal_format/double_literal_format_fix.dart b/lib/lints/double_literal_format/double_literal_format_fix.dart new file mode 100644 index 00000000..23c3043a --- /dev/null +++ b/lib/lints/double_literal_format/double_literal_format_fix.dart @@ -0,0 +1,64 @@ +part of 'double_literal_format_rule.dart'; + +/// A Quick fix for `double_literal_format` rule +/// Suggests the correct value for an issue +class _DoubleLiteralFormatFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addDoubleLiteral((node) { + // checks that the literal declaration is where our warning is located + if (!analysisError.sourceRange.intersects(node.sourceRange)) return; + + final lexeme = node.literal.lexeme; + String? correctLexeme; + + if (lexeme.hasLeadingZero) { + correctLexeme = _correctLeadingZeroLexeme(lexeme); + } else if (lexeme.hasLeadingDecimalPoint) { + correctLexeme = _correctLeadingDecimalPointLexeme(lexeme); + } else if (lexeme.hasTrailingZero) { + correctLexeme = _correctTrailingZeroLexeme(lexeme); + } + + if (correctLexeme != null) { + final changeBuilder = reporter.createChangeBuilder( + message: 'Replace by $correctLexeme', + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + SourceRange(node.offset, node.length), + correctLexeme!, + ); + }); + } + }); + } + + String _correctLeadingZeroLexeme(String lexeme) => !lexeme.hasLeadingZero + ? lexeme + : _correctLeadingZeroLexeme(lexeme.substring(1)); + + String _correctLeadingDecimalPointLexeme(String lexeme) => '0$lexeme'; + + String _correctTrailingZeroLexeme(String lexeme) { + if (!lexeme.hasTrailingZero) { + return lexeme; + } else { + final mantissa = lexeme.split('e').first; + return _correctTrailingZeroLexeme( + lexeme.replaceFirst( + mantissa, + mantissa.substring(0, mantissa.length - 1), + ), + ); + } + } +} diff --git a/lib/lints/double_literal_format/double_literal_format_rule.dart b/lib/lints/double_literal_format/double_literal_format_rule.dart new file mode 100644 index 00000000..5d1fea06 --- /dev/null +++ b/lib/lints/double_literal_format/double_literal_format_rule.dart @@ -0,0 +1,89 @@ +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +part 'double_literal_format_fix.dart'; +part 'double_literal_format_utils.dart'; + +/// A `double_literal_format` rule which +/// checks that double literals should begin with 0. instead of just ., +/// and should not end with a trailing 0. +/// BAD: +/// var a = 05.23, b = .16e+5, c = -0.250, d = -0.400e-5; +/// GOOD: +/// var a = 5.23, b = 0.16e+5, c = -0.25, d = -0.4e-5; +class DoubleLiteralFormatRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const lintName = 'double_literal_format'; + + // Use different messages for different issues + /// The [LintCode] of this lint rule that represents + /// the error whether we use double literals with a redundant leading 0. + static const _leadingZeroCode = LintCode( + name: lintName, + problemMessage: "Double literals shouldn't have redundant leading `0`.", + correctionMessage: "Remove redundant leading `0`.", + ); + + /// The [LintCode] of this lint rule that represents + /// the error whether we use double literals with a leading decimal point. + static const _leadingDecimalCode = LintCode( + name: lintName, + problemMessage: + "Double literals shouldn't begin with the decimal point `.`.", + correctionMessage: "Add missing leading `0`.", + ); + + /// The [LintCode] of this lint rule that represents + /// the error whether we use double literals with a trailing 0. + static const _trailingZeroCode = LintCode( + name: lintName, + problemMessage: "Double literals should not end with a trailing `0`.", + correctionMessage: "Remove redundant trailing `0`.", + ); + + DoubleLiteralFormatRule._(super.config); + + /// Creates a new instance of [DoubleLiteralFormatRule] + /// based on the lint configuration. + factory DoubleLiteralFormatRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => 'Double literal formatting issue', + ); + + return DoubleLiteralFormatRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addDoubleLiteral((node) { + final lexeme = node.literal.lexeme; + + if (lexeme.hasLeadingZero) { + reporter.reportErrorForNode(_leadingZeroCode, node); + return; + } + if (lexeme.hasLeadingDecimalPoint) { + reporter.reportErrorForNode(_leadingDecimalCode, node); + return; + } + if (lexeme.hasTrailingZero) { + reporter.reportErrorForNode(_trailingZeroCode, node); + return; + } + }); + } + + @override + List getFixes() => [_DoubleLiteralFormatFix()]; +} diff --git a/lib/lints/double_literal_format/double_literal_format_utils.dart b/lib/lints/double_literal_format/double_literal_format_utils.dart new file mode 100644 index 00000000..99e7166e --- /dev/null +++ b/lib/lints/double_literal_format/double_literal_format_utils.dart @@ -0,0 +1,19 @@ +part of 'double_literal_format_rule.dart'; + +/// Useful extensions for double literals representation +extension _StringDoubleEx on String { + /// Returns true if a double literal starts with 00 + bool get hasLeadingZero => startsWith('0') && this[1] != '.'; + + /// Returns true if a double literal starts with . + bool get hasLeadingDecimalPoint => startsWith('.'); + + /// Returns true if a mantissa of a double literal ends with 0 + bool get hasTrailingZero { + final mantissa = split('e').first; + + return mantissa.contains('.') && + mantissa.endsWith('0') && + mantissa.split('.').last != '0'; + } +} diff --git a/lib/lints/function_lines_of_code/function_lines_of_code_metric.dart b/lib/lints/function_lines_of_code/function_lines_of_code_metric.dart new file mode 100644 index 00000000..7a63796b --- /dev/null +++ b/lib/lints/function_lines_of_code/function_lines_of_code_metric.dart @@ -0,0 +1,53 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; +import 'package:solid_lints/lints/function_lines_of_code/visitor/function_lines_of_code_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A number of lines metric which checks whether we didn't exceed +/// the maximum allowed number of lines for a function. +class FunctionLinesOfCodeMetric + extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if number of + /// parameters reaches the maximum value. + static const lintName = 'function_lines_of_code'; + + FunctionLinesOfCodeMetric._(super.config); + + /// Creates a new instance of [FunctionLinesOfCodeMetric] + /// based on the lint configuration. + factory FunctionLinesOfCodeMetric.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + paramsParser: FunctionLinesOfCodeParameters.fromJson, + problemMessage: (value) => + 'The maximum allowed number of lines is ${value.maxLines}. ' + 'Try splitting this function into smaller parts.', + ); + + return FunctionLinesOfCodeMetric._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + final visitor = FunctionLinesOfCodeVisitor(resolver.lineInfo); + + context.registry.addDeclaration((node) { + node.visitChildren(visitor); + + if (visitor.linesWithCode.length > config.parameters.maxLines) { + reporter.reportErrorForOffset( + code, + node.firstTokenAfterCommentAndMetadata.offset, + node.end, + ); + } + }); + } +} diff --git a/lib/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart b/lib/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart new file mode 100644 index 00000000..48b6b0fd --- /dev/null +++ b/lib/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart @@ -0,0 +1,19 @@ +/// A data model class that represents the "function lines of code" input +/// parameters. +class FunctionLinesOfCodeParameters { + /// Maximum number of lines + final int maxLines; + + static const _defaultMaxLines = 200; + + /// Constructor for [FunctionLinesOfCodeParameters] model + const FunctionLinesOfCodeParameters({ + required this.maxLines, + }); + + /// Method for creating from json data + factory FunctionLinesOfCodeParameters.fromJson(Map json) => + FunctionLinesOfCodeParameters( + maxLines: json['max_lines'] as int? ?? _defaultMaxLines, + ); +} diff --git a/lib/lints/function_lines_of_code/visitor/function_lines_of_code_visitor.dart b/lib/lints/function_lines_of_code/visitor/function_lines_of_code_visitor.dart new file mode 100644 index 00000000..d3596c3f --- /dev/null +++ b/lib/lints/function_lines_of_code/visitor/function_lines_of_code_visitor.dart @@ -0,0 +1,69 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/source/line_info.dart'; + +/// The AST visitor that will find lines with code. +class FunctionLinesOfCodeVisitor extends RecursiveAstVisitor { + final LineInfo _lineInfo; + + final _linesWithCode = {}; + + /// Returns the array with indices of lines with code. + Iterable get linesWithCode => _linesWithCode; + + /// Creates a new instance of [FunctionLinesOfCodeVisitor]. + FunctionLinesOfCodeVisitor(this._lineInfo); + + @override + void visitBlockFunctionBody(BlockFunctionBody node) { + _collectFunctionBodyData( + node.block.leftBracket.next, + node.block.rightBracket, + ); + super.visitBlockFunctionBody(node); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + _collectFunctionBodyData( + node.expression.beginToken.previous, + node.expression.endToken.next, + ); + super.visitExpressionFunctionBody(node); + } + + void _collectFunctionBodyData(Token? firstToken, Token? lastToken) { + var token = firstToken; + while (token != lastToken && token != null) { + if (!token.isSynthetic) { + _linesWithCode.add(_lineInfo.getLocation(token.offset).lineNumber); + } + + token = token.next; + } + } +} diff --git a/lib/lints/member_ordering/config_parser.dart b/lib/lints/member_ordering/config_parser.dart new file mode 100644 index 00000000..e543abab --- /dev/null +++ b/lib/lints/member_ordering/config_parser.dart @@ -0,0 +1,167 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:collection/collection.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/field_keyword.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/constructor_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/field_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/get_set_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/method_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Helper class to parse member_ordering rule config +class MemberOrderingConfigParser { + static const _defaultOrderList = [ + 'public_fields', + 'private_fields', + 'public_getters', + 'private_getters', + 'public_setters', + 'private_setters', + 'constructors', + 'public_methods', + 'private_methods', + ]; + + static const _defaultWidgetsOrderList = [ + 'constructor', + 'named_constructor', + 'const_fields', + 'static_methods', + 'final_fields', + 'init_state_method', + 'var_fields', + 'init_state_method', + 'private_methods', + 'overridden_public_methods', + 'build_method', + ]; + + static final _regExp = RegExp( + '(overridden_|protected_)?(private_|public_)?(static_)?(late_)?' + '(var_|final_|const_)?(nullable_)?(named_)?(factory_)?', + ); + + /// Parse rule config for regular class order rules + static List parseOrder(Object? orderConfig) { + final order = orderConfig is Iterable + ? List.from(orderConfig) + : _defaultOrderList; + + return order.map(_parseGroup).whereNotNull().toList(); + } + + /// Parse rule config for widget class order rules + static List parseWidgetsOrder(Object? widgetsOrderConfig) { + final widgetsOrder = widgetsOrderConfig is Iterable + ? List.from(widgetsOrderConfig) + : _defaultWidgetsOrderList; + + return widgetsOrder.map(_parseGroup).whereNotNull().toList(); + } + + static MemberGroup? _parseGroup(String group) { + final lastGroup = group.endsWith('getters_setters') + ? 'getters_setters' + : group.split('_').lastOrNull; + final type = MemberType.parse(lastGroup); + final result = _regExp.allMatches(group.toLowerCase()); + + final isNamedMethod = group.endsWith('_method'); + if (isNamedMethod) { + final name = group.split('_method').first.replaceAll('_', ''); + + return MethodMemberGroup.named( + name: name, + memberType: MemberType.method, + rawRepresentation: group, + ); + } + + final hasGroups = result.isNotEmpty && result.first.groupCount > 0; + if (hasGroups && type != null) { + final match = result.first; + + final annotation = Annotation.parse(match.group(1)?.replaceAll('_', '')); + final modifier = Modifier.parse(match.group(2)?.replaceAll('_', '')); + final isStatic = match.group(3) != null; + final isLate = match.group(4) != null; + final keyword = FieldKeyword.parse(match.group(5)?.replaceAll('_', '')); + final isNullable = match.group(6) != null; + final isNamed = match.group(7) != null; + final isFactory = match.group(8) != null; + + switch (type) { + case MemberType.field: + return FieldMemberGroup( + isLate: isLate, + isNullable: isNullable, + isStatic: isStatic, + keyword: keyword, + annotation: annotation, + memberType: type, + modifier: modifier, + rawRepresentation: group, + ); + + case MemberType.method: + return MethodMemberGroup( + name: null, + isNullable: isNullable, + isStatic: isStatic, + annotation: annotation, + memberType: type, + modifier: modifier, + rawRepresentation: group, + ); + + case MemberType.getter: + case MemberType.setter: + case MemberType.getterAndSetter: + return GetSetMemberGroup( + isNullable: isNullable, + isStatic: isStatic, + annotation: annotation, + memberType: type, + modifier: modifier, + rawRepresentation: group, + ); + + case MemberType.constructor: + return ConstructorMemberGroup( + isNamed: isFactory || isNamed, + isFactory: isFactory, + annotation: annotation, + memberType: type, + modifier: modifier, + rawRepresentation: group, + ); + } + } + + return null; + } +} diff --git a/lib/lints/member_ordering/member_ordering_rule.dart b/lib/lints/member_ordering/member_ordering_rule.dart new file mode 100644 index 00000000..d341587f --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_rule.dart @@ -0,0 +1,120 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/member_ordering/member_ordering_visitor.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_ordering_parameters.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `member_ordering` rule which +/// warns about class members being in wrong order +/// Custom order can be provided through config +class MemberOrderingRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether we use bad formatted double literals. + static const lintName = 'member_ordering'; + + static const _warningMessage = 'should be before'; + static const _warningAlphabeticalMessage = 'should be alphabetically before'; + static const _warningTypeAlphabeticalMessage = + 'type name should be alphabetically before'; + + MemberOrderingRule._(super.config); + + /// Creates a new instance of [MemberOrderingRule] + /// based on the lint configuration. + factory MemberOrderingRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + paramsParser: MemberOrderingParameters.fromJson, + problemMessage: (_) => "Order of class member is wrong", + ); + + return MemberOrderingRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addClassDeclaration((node) { + final visitor = MemberOrderingVisitor( + config.parameters.groupsOrder, + config.parameters.widgetsGroupsOrder, + ); + + final membersInfo = visitor.visitClassDeclaration(node); + final wrongOrderMembers = membersInfo.where( + (info) => info.memberOrder.isWrong, + ); + + for (final memberInfo in wrongOrderMembers) { + reporter.reportErrorForNode( + _createWrongOrderLintCode(memberInfo), + memberInfo.classMember, + ); + } + + if (config.parameters.alphabetize) { + final alphabeticallyWrongOrderMembers = membersInfo.where( + (info) => info.memberOrder.isAlphabeticallyWrong, + ); + + for (final memberInfo in alphabeticallyWrongOrderMembers) { + reporter.reportErrorForNode( + _createAlphabeticallyWrongOrderLintCode(memberInfo), + memberInfo.classMember, + ); + } + } + + if (!config.parameters.alphabetize && + config.parameters.alphabetizeByType) { + final alphabeticallyByTypeWrongOrderMembers = membersInfo.where( + (info) => info.memberOrder.isByTypeWrong, + ); + + for (final memberInfo in alphabeticallyByTypeWrongOrderMembers) { + reporter.reportErrorForNode( + _createAlphabeticallyByTypeWrongOrderLintCode(memberInfo), + memberInfo.classMember, + ); + } + } + }); + } + + LintCode _createWrongOrderLintCode(MemberInfo info) { + final memberGroup = info.memberOrder.memberGroup; + final previousMemberGroup = info.memberOrder.previousMemberGroup; + + return LintCode( + name: lintName, + problemMessage: "$memberGroup $_warningMessage $previousMemberGroup.", + ); + } + + LintCode _createAlphabeticallyWrongOrderLintCode(MemberInfo info) { + final names = info.memberOrder.memberNames; + final current = names.currentName; + final previous = names.previousName; + + return LintCode( + name: lintName, + problemMessage: "$current $_warningAlphabeticalMessage $previous.", + ); + } + + LintCode _createAlphabeticallyByTypeWrongOrderLintCode(MemberInfo info) { + final names = info.memberOrder.memberNames; + final current = names.currentName; + final previous = names.previousName; + + return LintCode( + name: lintName, + problemMessage: "$current $_warningTypeAlphabeticalMessage $previous", + ); + } +} diff --git a/lib/lints/member_ordering/member_ordering_utils.dart b/lib/lints/member_ordering/member_ordering_utils.dart new file mode 100644 index 00000000..d1821f79 --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_utils.dart @@ -0,0 +1,21 @@ +import 'package:analyzer/dart/ast/ast.dart' show AnnotatedNode, Identifier; +import 'package:collection/collection.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Parses [AnnotatedNode] and creates an instance of [Annotation] +Annotation? parseAnnotation(AnnotatedNode node) { + return node.metadata + .map((metadata) => Annotation.parse(metadata.name.name)) + .whereNotNull() + .firstOrNull; +} + +/// Parses class memberName and return it's access [Modifier] +Modifier parseModifier(String? memberName) { + if (memberName == null) return Modifier.unset; + + return Identifier.isPrivateName(memberName) + ? Modifier.private + : Modifier.public; +} diff --git a/lib/lints/member_ordering/member_ordering_visitor.dart b/lib/lints/member_ordering/member_ordering_visitor.dart new file mode 100644 index 00000000..071a9c7b --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_visitor.dart @@ -0,0 +1,362 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart' hide Annotation; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/field_keyword.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/constructor_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/field_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/get_set_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/method_member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// AST Visitor which finds all class members and checks if they are +/// in order provided from rule config or default config +class MemberOrderingVisitor extends RecursiveAstVisitor> { + final List _groupsOrder; + final List _widgetsGroupsOrder; + + final _membersInfo = []; + + /// Creates instance of [MemberOrderingVisitor] + /// [_groupsOrder] config is used for regular classes + /// [_widgetsGroupsOrder] config is used for widget classes + MemberOrderingVisitor(this._groupsOrder, this._widgetsGroupsOrder); + + @override + List visitClassDeclaration(ClassDeclaration node) { + super.visitClassDeclaration(node); + + _membersInfo.clear(); + + final type = node.extendsClause?.superclass.type; + final isFlutterWidget = + isWidgetOrSubclass(type) || isWidgetStateOrSubclass(type); + + for (final member in node.members) { + if (member is FieldDeclaration) { + _visitFieldDeclaration(member, isFlutterWidget); + } else if (member is ConstructorDeclaration) { + _visitConstructorDeclaration(member, isFlutterWidget); + } else if (member is MethodDeclaration) { + _visitMethodDeclaration(member, isFlutterWidget); + } + } + + return _membersInfo; + } + + void _visitFieldDeclaration( + FieldDeclaration declaration, + bool isFlutterWidget, + ) { + final group = FieldMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.fields.variables.first.name.lexeme, + declaration.fields.type?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } + + void _visitConstructorDeclaration( + ConstructorDeclaration declaration, + bool isFlutterWidget, + ) { + final group = ConstructorMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name?.lexeme ?? '', + declaration.returnType.name, + isFlutterWidget, + ), + ), + ); + } + } + + void _visitMethodDeclaration( + MethodDeclaration declaration, + bool isFlutterWidget, + ) { + if (declaration.isGetter || declaration.isSetter) { + final group = GetSetMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name.lexeme, + declaration.returnType?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } else { + final group = MethodMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name.lexeme, + declaration.returnType?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } + } + + MemberGroup? _getClosestGroup( + MemberGroup parsedGroup, + bool isFlutterWidget, + ) { + final closestGroups = (isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder) + .where( + (group) => + _isConstructorGroup(group, parsedGroup) || + _isFieldGroup(group, parsedGroup) || + _isGetSetGroup(group, parsedGroup) || + _isMethodGroup(group, parsedGroup), + ) + .sorted( + (a, b) => b.getSortingCoefficient() - a.getSortingCoefficient(), + ); + + return closestGroups.firstOrNull; + } + + MemberOrder _getOrder( + MemberGroup memberGroup, + String memberName, + String typeName, + bool isFlutterWidget, + ) { + if (_membersInfo.isNotEmpty) { + final lastMemberOrder = _membersInfo.last.memberOrder; + final hasSameGroup = lastMemberOrder.memberGroup == memberGroup; + + final previousMemberGroup = + hasSameGroup && lastMemberOrder.previousMemberGroup != null + ? lastMemberOrder.previousMemberGroup + : lastMemberOrder.memberGroup; + + final memberNames = MemberNames( + currentName: memberName, + previousName: lastMemberOrder.memberNames.currentName, + currentTypeName: typeName, + previousTypeName: lastMemberOrder.memberNames.currentTypeName, + ); + + return MemberOrder( + memberNames: memberNames, + isAlphabeticallyWrong: hasSameGroup && + memberNames.currentName.compareTo(memberNames.previousName!) < 0, + isByTypeWrong: hasSameGroup && + memberNames.currentTypeName + .toLowerCase() + .compareTo(memberNames.previousTypeName!.toLowerCase()) < + 0, + memberGroup: memberGroup, + previousMemberGroup: previousMemberGroup, + isWrong: (hasSameGroup && lastMemberOrder.isWrong) || + _isCurrentGroupBefore( + lastMemberOrder.memberGroup, + memberGroup, + isFlutterWidget, + ), + ); + } + + return MemberOrder( + memberNames: + MemberNames(currentName: memberName, currentTypeName: typeName), + isAlphabeticallyWrong: false, + isByTypeWrong: false, + memberGroup: memberGroup, + isWrong: false, + ); + } + + bool _isCurrentGroupBefore( + MemberGroup lastMemberGroup, + MemberGroup memberGroup, + bool isFlutterWidget, + ) { + final group = isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder; + + return group.indexOf(lastMemberGroup) > group.indexOf(memberGroup); + } + + bool _isConstructorGroup(MemberGroup group, MemberGroup parsedGroup) => + group is ConstructorMemberGroup && + parsedGroup is ConstructorMemberGroup && + (!group.isFactory || group.isFactory == parsedGroup.isFactory) && + (!group.isNamed || group.isNamed == parsedGroup.isNamed) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isMethodGroup(MemberGroup group, MemberGroup parsedGroup) => + group is MethodMemberGroup && + parsedGroup is MethodMemberGroup && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.name == null || group.name == parsedGroup.name) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isGetSetGroup(MemberGroup group, MemberGroup parsedGroup) => + group is GetSetMemberGroup && + parsedGroup is GetSetMemberGroup && + (group.memberType == parsedGroup.memberType || + (group.memberType == MemberType.getterAndSetter && + (parsedGroup.memberType == MemberType.getter || + parsedGroup.memberType == MemberType.setter))) && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isFieldGroup(MemberGroup group, MemberGroup parsedGroup) => + group is FieldMemberGroup && + parsedGroup is FieldMemberGroup && + (!group.isLate || group.isLate == parsedGroup.isLate) && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.keyword == FieldKeyword.unset || + group.keyword == parsedGroup.keyword) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); +} + +/// Data class that holds AST class member and it's order info +class MemberInfo { + /// AST instance of an [ClassMember] + final ClassMember classMember; + + /// Class member order info + final MemberOrder memberOrder; + + /// Creates instance of an [MemberInfo] + const MemberInfo({ + required this.classMember, + required this.memberOrder, + }); +} + +/// Data class holds information about class member order info +class MemberOrder { + /// Indicates if order is wrong + final bool isWrong; + + /// Indicates if order is wrong alphabetically + final bool isAlphabeticallyWrong; + + /// Indicates if order is wrong alphabetically by type + final bool isByTypeWrong; + + /// Info about current and previous class member name + final MemberNames memberNames; + + /// Info about current member member group + final MemberGroup memberGroup; + + /// Info about previous member member group + final MemberGroup? previousMemberGroup; + + /// Creates instance of [MemberOrder] + const MemberOrder({ + required this.isWrong, + required this.isAlphabeticallyWrong, + required this.isByTypeWrong, + required this.memberNames, + required this.memberGroup, + this.previousMemberGroup, + }); +} + +/// Data class contains info about current and previous class member names +class MemberNames { + /// Name of current class member + final String currentName; + + /// Name of previous class member + final String? previousName; + + /// Type name of current class member + final String currentTypeName; + + /// Type name of previous class member + final String? previousTypeName; + + /// Crates instance of [MemberNames] + const MemberNames({ + required this.currentName, + required this.currentTypeName, + this.previousName, + this.previousTypeName, + }); +} diff --git a/lib/lints/member_ordering/models/annotation.dart b/lib/lints/member_ordering/models/annotation.dart new file mode 100644 index 00000000..1e16249c --- /dev/null +++ b/lib/lints/member_ordering/models/annotation.dart @@ -0,0 +1,51 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// A data model enum represents annotation +enum Annotation { + /// override annotation + override('override', 'overridden'), + + /// protected annotation + protected('protected'), + + /// Indicates missing annotation + /// used to handle cases of unsupported annotations + unset('unset'); + + /// String representation of name of an annotation + final String name; + + /// String representation of public name of an annotation + final String? publicName; + + const Annotation(this.name, [this.publicName]); + + /// Parses a String name and returns instance of [Annotation] + static Annotation parse(String? name) => values.firstWhere( + (annotation) => + annotation.name == name || + (annotation.publicName != null && annotation.publicName == name), + orElse: () => Annotation.unset, + ); +} diff --git a/lib/lints/member_ordering/models/field_keyword.dart b/lib/lints/member_ordering/models/field_keyword.dart new file mode 100644 index 00000000..d2716ca2 --- /dev/null +++ b/lib/lints/member_ordering/models/field_keyword.dart @@ -0,0 +1,49 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// A data model enum represents field type keyword +enum FieldKeyword { + /// final keyword + isFinal('final'), + + /// const keyword + isConst('const'), + + /// var keyword + isVar('var'), + + /// Indicates missing field keyword + /// used to handle cases of unsupported field type keywords + unset('unset'); + + /// String representation of field type keyword + final String type; + + const FieldKeyword(this.type); + + /// Parses a String field type and returns instance of [FieldKeyword] + static FieldKeyword parse(String? name) => values.firstWhere( + (type) => type.type == name, + orElse: () => FieldKeyword.unset, + ); +} diff --git a/lib/lints/member_ordering/models/member_group/constructor_member_group.dart b/lib/lints/member_ordering/models/member_group/constructor_member_group.dart new file mode 100644 index 00000000..7086a74c --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/constructor_member_group.dart @@ -0,0 +1,83 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart' show ConstructorDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class constructor +class ConstructorMemberGroup extends MemberGroup { + /// Shows if constructor is named + final bool isNamed; + + /// Shows if constructor is factory constructor + final bool isFactory; + + /// Creates instance of [ConstructorMemberGroup] + const ConstructorMemberGroup({ + required this.isNamed, + required this.isFactory, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses instance of [ConstructorDeclaration] + /// and returns [ConstructorMemberGroup] + factory ConstructorMemberGroup.parse(ConstructorDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final name = declaration.name; + final isFactory = declaration.factoryKeyword != null; + final isNamed = name != null; + + final modifier = parseModifier(name?.lexeme); + + return ConstructorMemberGroup( + isNamed: isNamed, + isFactory: isFactory, + annotation: annotation ?? Annotation.unset, + modifier: modifier, + memberType: MemberType.constructor, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isNamed ? 1 : 0; + coefficient += isFactory ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_group/field_member_group.dart b/lib/lints/member_ordering/models/member_group/field_member_group.dart new file mode 100644 index 00000000..e1a223ec --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/field_member_group.dart @@ -0,0 +1,107 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart' show FieldDeclaration; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/field_keyword.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class field +class FieldMemberGroup extends MemberGroup { + /// Shows if field is static member of class + final bool isStatic; + + /// Shows if field is nullable type + final bool isNullable; + + /// Show if field is late initialization + final bool isLate; + + /// Represents field keyword + final FieldKeyword keyword; + + /// Creates instance of [FieldMemberGroup] + const FieldMemberGroup({ + required this.isLate, + required this.isNullable, + required this.isStatic, + required this.keyword, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses [FieldDeclaration] and creates instance of [FieldMemberGroup] + factory FieldMemberGroup.parse(FieldDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final modifier = parseModifier( + declaration.fields.variables.first.name.lexeme, + ); + final isNullable = declaration.fields.type?.type?.nullabilitySuffix == + NullabilitySuffix.question; + final keyword = _FieldMemberGroupUtils.parseKeyWord(declaration); + + return FieldMemberGroup( + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: isNullable, + isLate: declaration.fields.isLate, + memberType: MemberType.field, + modifier: modifier, + keyword: keyword, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += isLate ? 1 : 0; + coefficient += keyword != FieldKeyword.unset ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} + +class _FieldMemberGroupUtils { + static FieldKeyword parseKeyWord(FieldDeclaration declaration) { + return declaration.fields.isConst + ? FieldKeyword.isConst + : declaration.fields.isFinal + ? FieldKeyword.isFinal + : FieldKeyword.unset; + } +} diff --git a/lib/lints/member_ordering/models/member_group/get_set_member_group.dart b/lib/lints/member_ordering/models/member_group/get_set_member_group.dart new file mode 100644 index 00000000..abd16b4e --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/get_set_member_group.dart @@ -0,0 +1,79 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart' show MethodDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class getter or setter +class GetSetMemberGroup extends MemberGroup { + /// Shows if getter/setter is static class member + final bool isStatic; + + /// Show if return type of getter/setter is nullable type + final bool isNullable; + + /// Creates instance of [GetSetMemberGroup] + const GetSetMemberGroup({ + required this.isNullable, + required this.isStatic, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses [MethodDeclaration] and returns instance of [GetSetMemberGroup] + factory GetSetMemberGroup.parse(MethodDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final type = declaration.isGetter ? MemberType.getter : MemberType.setter; + final modifier = parseModifier(declaration.name.lexeme); + + return GetSetMemberGroup( + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: declaration.returnType?.question != null, + memberType: type, + modifier: modifier, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_group/member_group.dart b/lib/lints/member_ordering/models/member_group/member_group.dart new file mode 100644 index 00000000..b35990d7 --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/member_group.dart @@ -0,0 +1,52 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Abstract class representing class member group +abstract class MemberGroup { + /// Member annotation (e.g. override, protected) + final Annotation annotation; + + /// Member type (e.g. field, method) + final MemberType memberType; + + /// Member access modifier (e.g. public, private) + final Modifier modifier; + + /// Raw String representation of class member group + final String rawRepresentation; + + /// Constructor to use in children extending [MemberGroup] + const MemberGroup({ + required this.annotation, + required this.memberType, + required this.modifier, + required this.rawRepresentation, + }); + + /// Method to get sorting coefficient of the member group + int getSortingCoefficient(); +} diff --git a/lib/lints/member_ordering/models/member_group/method_member_group.dart b/lib/lints/member_ordering/models/member_group/method_member_group.dart new file mode 100644 index 00000000..9a0ac33e --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/method_member_group.dart @@ -0,0 +1,103 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart' show Identifier, MethodDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class method +class MethodMemberGroup extends MemberGroup { + /// Shows if method is static member of a class + final bool isStatic; + + /// Shows if method return type is nullable + final bool isNullable; + + /// Represents method name + final String? name; + + /// Creates instance of [MethodMemberGroup] + const MethodMemberGroup({ + required this.isNullable, + required this.isStatic, + required this.name, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Named constructor to create an instance of [MethodMemberGroup] with name + factory MethodMemberGroup.named({ + required String name, + required MemberType memberType, + required String rawRepresentation, + }) => + MethodMemberGroup( + name: name, + isNullable: false, + isStatic: false, + modifier: Modifier.unset, + annotation: Annotation.unset, + memberType: memberType, + rawRepresentation: rawRepresentation, + ); + + /// Parses [MethodDeclaration] and returns instance of [MethodMemberGroup] + factory MethodMemberGroup.parse(MethodDeclaration declaration) { + final methodName = declaration.name.lexeme; + final annotation = parseAnnotation(declaration); + final modifier = Identifier.isPrivateName(methodName) + ? Modifier.private + : Modifier.public; + + return MethodMemberGroup( + name: methodName.toLowerCase(), + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: declaration.returnType?.question != null, + memberType: MemberType.method, + modifier: modifier, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + coefficient += name != null ? 10 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_ordering_parameters.dart b/lib/lints/member_ordering/models/member_ordering_parameters.dart new file mode 100644 index 00000000..ceb614fb --- /dev/null +++ b/lib/lints/member_ordering/models/member_ordering_parameters.dart @@ -0,0 +1,66 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:solid_lints/lints/member_ordering/config_parser.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; + +/// A data model class that represents the member ordering input +/// parameters. +class MemberOrderingParameters { + /// Config keys + static const _orderConfig = 'order'; + static const _widgetsOrderConfig = 'widgets_order'; + static const _alphabetizeConfig = 'alphabetize'; + static const _alphabetizeByTypeConfig = 'alphabetize_by_type'; + + /// Config used for members of regular class + final List groupsOrder; + + /// Config used for members of widget class + final List widgetsGroupsOrder; + + /// Indicates if params should be in alphabetical order + final bool alphabetize; + + /// Indicates if params should be in alphabetical order of their type + final bool alphabetizeByType; + + /// Constructor for [MemberOrderingParameters] model + const MemberOrderingParameters({ + required this.groupsOrder, + required this.widgetsGroupsOrder, + required this.alphabetize, + required this.alphabetizeByType, + }); + + /// Method for creating from json data + factory MemberOrderingParameters.fromJson(Map json) => + MemberOrderingParameters( + groupsOrder: MemberOrderingConfigParser.parseOrder(json[_orderConfig]), + widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder( + json[_widgetsOrderConfig], + ), + alphabetize: json[_alphabetizeConfig] as bool? ?? false, + alphabetizeByType: json[_alphabetizeByTypeConfig] as bool? ?? false, + ); +} diff --git a/lib/lints/member_ordering/models/member_type.dart b/lib/lints/member_ordering/models/member_type.dart new file mode 100644 index 00000000..e978dd7e --- /dev/null +++ b/lib/lints/member_ordering/models/member_type.dart @@ -0,0 +1,57 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:collection/collection.dart'; + +/// A data model enum represents class member type affiliation +enum MemberType { + /// Indicates fields affiliation + field('fields'), + + /// Indicates method affiliation + method('methods', typeAlias: 'method'), + + /// Indicates constructor affiliation + constructor('constructors'), + + /// Indicates getters affiliation + getter('getters'), + + /// Indicates setters affiliation + setter('setters'), + + /// Indicates affiliation with both getters and setters + getterAndSetter('getters-setters'); + + /// String representation of member group type + final String type; + + /// Alternative string representation of member group type + final String? typeAlias; + + const MemberType(this.type, {this.typeAlias}); + + /// Parses a String member type and returns instance of [MemberType] + static MemberType? parse(String? name) => values + .firstWhereOrNull((type) => name == type.type || name == type.typeAlias); +} diff --git a/lib/lints/member_ordering/models/modifier.dart b/lib/lints/member_ordering/models/modifier.dart new file mode 100644 index 00000000..b47b7f89 --- /dev/null +++ b/lib/lints/member_ordering/models/modifier.dart @@ -0,0 +1,44 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// A data model enum represents class member access modifier +enum Modifier { + /// Indicates public access modifier + public('public'), + + /// Indicates private access modifier + private('private'), + + /// Indicates missing access modifier + /// used to handle cases of unsupported field type keywords + unset('unset'); + + /// String representation of access modifier keyword + final String type; + + const Modifier(this.type); + + /// Parses a String access modifier and returns instance of [Modifier] + static Modifier parse(String? name) => values + .firstWhere((type) => type.type == name, orElse: () => Modifier.unset); +} diff --git a/lib/lints/newline_before_return/newline_before_return_rule.dart b/lib/lints/newline_before_return/newline_before_return_rule.dart new file mode 100644 index 00000000..9e8a19ab --- /dev/null +++ b/lib/lints/newline_before_return/newline_before_return_rule.dart @@ -0,0 +1,68 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/newline_before_return/newline_before_return_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +// Inspired by ESLint (https://eslint.org/docs/rules/newline-before-return) + +/// A `newline_before_return` rule which +/// warns about missing newline before return +class NewlineBeforeReturnRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if + /// newline is missing before return statement + static const String lintName = 'newline_before_return'; + + NewlineBeforeReturnRule._(super.config); + + /// Creates a new instance of [NewlineBeforeReturnRule] + /// based on the lint configuration. + factory NewlineBeforeReturnRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => "Missing blank line before return.", + ); + + return NewlineBeforeReturnRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addReturnStatement((node) { + final visitor = NewLineBeforeReturnVisitor(resolver.lineInfo); + visitor.visitReturnStatement(node); + + for (final element in visitor.statements) { + reporter.reportErrorForNode(code, element); + } + }); + } +} diff --git a/lib/lints/newline_before_return/newline_before_return_visitor.dart b/lib/lints/newline_before_return/newline_before_return_visitor.dart new file mode 100644 index 00000000..cad8b50a --- /dev/null +++ b/lib/lints/newline_before_return/newline_before_return_visitor.dart @@ -0,0 +1,93 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/source/line_info.dart'; + +/// The AST visitor that will all return statements. +class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { + final LineInfo _lineInfo; + final _statements = []; + + /// Creates instance of [NewLineBeforeReturnVisitor] with line info + NewLineBeforeReturnVisitor(this._lineInfo); + + /// List of all return statements + Iterable get statements => _statements; + + @override + void visitReturnStatement(ReturnStatement node) { + super.visitReturnStatement(node); + + if (!_statementIsInBlock(node)) return; + if (_statementIsFirstInBlock(node)) return; + if (_statementHasNewLineBefore(node, _lineInfo)) return; + + _statements.add(node); + } + + static bool _statementIsInBlock(ReturnStatement node) => node.parent is Block; + + static bool _statementIsFirstInBlock(ReturnStatement node) => + node.returnKeyword.previous == node.parent?.beginToken; + + static bool _statementHasNewLineBefore( + ReturnStatement node, + LineInfo lineInfo, + ) { + final previousTokenLineNumber = + lineInfo.getLocation(node.returnKeyword.previous!.end).lineNumber; + + final lastNotEmptyLineToken = _optimalToken(node.returnKeyword, lineInfo); + final tokenLineNumber = + lineInfo.getLocation(lastNotEmptyLineToken.offset).lineNumber; + + return tokenLineNumber > previousTokenLineNumber + 1; + } + + /// If return statement has comment above ignores all the comment lines + static Token _optimalToken(Token token, LineInfo lineInfo) { + var optimalToken = token; + + var commentToken = _latestCommentToken(token); + while (commentToken != null && + lineInfo.getLocation(commentToken.end).lineNumber + 1 >= + lineInfo.getLocation(optimalToken.offset).lineNumber) { + optimalToken = commentToken; + commentToken = commentToken.previous; + } + + return optimalToken; + } + + static Token? _latestCommentToken(Token token) { + Token? latestCommentToken = token.precedingComments; + while (latestCommentToken?.next != null) { + latestCommentToken = latestCommentToken?.next; + } + + return latestCommentToken; + } +} diff --git a/lib/lints/no_empty_block/no_empty_block_rule.dart b/lib/lints/no_empty_block/no_empty_block_rule.dart new file mode 100644 index 00000000..3524a087 --- /dev/null +++ b/lib/lints/no_empty_block/no_empty_block_rule.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/no_empty_block/no_empty_block_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +// Inspired by TSLint (https://palantir.github.io/tslint/rules/no-empty/) + +/// A `no_empty_block` rule which forbids having empty blocks. +/// Excluding catch blocks and to-do comments +class NoEmptyBlockRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether left empty block. + static const String lintName = 'no_empty_block'; + + NoEmptyBlockRule._(super.config); + + /// Creates a new instance of [NoEmptyBlockRule] + /// based on the lint configuration. + factory NoEmptyBlockRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (_) => + 'Block is empty. Empty blocks are often indicators of missing code.', + ); + + return NoEmptyBlockRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = NoEmptyBlockVisitor(); + node.accept(visitor); + + for (final emptyBlock in visitor.emptyBlocks) { + reporter.reportErrorForNode(code, emptyBlock); + } + }); + } +} diff --git a/lib/lints/no_empty_block/no_empty_block_visitor.dart b/lib/lints/no_empty_block/no_empty_block_visitor.dart new file mode 100644 index 00000000..3248f3bd --- /dev/null +++ b/lib/lints/no_empty_block/no_empty_block_visitor.dart @@ -0,0 +1,50 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +const _todoComment = 'TODO'; + +/// The AST visitor that will find all empty blocks, excluding catch blocks +/// and blocks containing [_todoComment] +class NoEmptyBlockVisitor extends RecursiveAstVisitor { + final _emptyBlocks = []; + + /// All empty blocks + Iterable get emptyBlocks => _emptyBlocks; + + @override + void visitBlock(Block node) { + super.visitBlock(node); + + if (node.statements.isNotEmpty) return; + if (node.parent is CatchClause) return; + if (_isPrecedingCommentToDo(node)) return; + + _emptyBlocks.add(node); + } + + static bool _isPrecedingCommentToDo(Block node) => + node.endToken.precedingComments?.lexeme.contains(_todoComment) ?? false; +} diff --git a/lib/lints/no_equal_then_else/no_equal_then_else_rule.dart b/lib/lints/no_equal_then_else/no_equal_then_else_rule.dart new file mode 100644 index 00000000..34a7f4c2 --- /dev/null +++ b/lib/lints/no_equal_then_else/no_equal_then_else_rule.dart @@ -0,0 +1,45 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/no_equal_then_else/no_equal_then_else_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +// Inspired by PVS-Studio (https://www.viva64.com/en/w/v6004/) + +/// A `no_equal_then_else` rule which warns about +/// unnecessary if statements and conditional expressions +class NoEqualThenElseRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if + /// 'if' statements or conditional expression is redundant + static const String lintName = 'no_equal_then_else'; + + NoEqualThenElseRule._(super.config); + + /// Creates a new instance of [NoEqualThenElseRule] + /// based on the lint configuration. + factory NoEqualThenElseRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => "Then and else branches are equal.", + ); + + return NoEqualThenElseRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = NoEqualThenElseVisitor(); + node.accept(visitor); + + for (final element in visitor.nodes) { + reporter.reportErrorForNode(code, element); + } + }); + } +} diff --git a/lib/lints/no_equal_then_else/no_equal_then_else_visitor.dart b/lib/lints/no_equal_then_else/no_equal_then_else_visitor.dart new file mode 100644 index 00000000..0ec18307 --- /dev/null +++ b/lib/lints/no_equal_then_else/no_equal_then_else_visitor.dart @@ -0,0 +1,54 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// The AST visitor that will collect all unnecessary if statements and +/// conditional expressions. +class NoEqualThenElseVisitor extends RecursiveAstVisitor { + final _nodes = []; + + /// All unnecessary if statements and conditional expressions. + Iterable get nodes => _nodes; + + @override + void visitIfStatement(IfStatement node) { + super.visitIfStatement(node); + + if (node.elseStatement != null && + node.elseStatement is! IfStatement && + node.thenStatement.toString() == node.elseStatement.toString()) { + _nodes.add(node); + } + } + + @override + void visitConditionalExpression(ConditionalExpression node) { + super.visitConditionalExpression(node); + + if (node.thenExpression.toString() == node.elseExpression.toString()) { + _nodes.add(node); + } + } +} diff --git a/lib/lints/no_magic_number/models/no_magic_number_parameters.dart b/lib/lints/no_magic_number/models/no_magic_number_parameters.dart new file mode 100644 index 00000000..3a51f439 --- /dev/null +++ b/lib/lints/no_magic_number/models/no_magic_number_parameters.dart @@ -0,0 +1,29 @@ +/// A data model class that represents the "no magic numbers" input +/// parameters. +class NoMagicNumberParameters { + static const _allowedConfigName = 'allowed'; + static const _allowedInWidgetParamsConfigName = 'allowed_in_widget_params'; + static const _defaultMagicNumbers = [-1, 0, 1]; + + /// List of allowed numbers + final Iterable allowedNumbers; + + /// The flag indicates whether magic numbers are allowed as a Widget instance + /// parameter. + final bool allowedInWidgetParams; + + /// Constructor for [NoMagicNumberParameters] model + const NoMagicNumberParameters({ + required this.allowedNumbers, + required this.allowedInWidgetParams, + }); + + /// Method for creating from json data + factory NoMagicNumberParameters.fromJson(Map json) => + NoMagicNumberParameters( + allowedNumbers: + json[_allowedConfigName] as Iterable? ?? _defaultMagicNumbers, + allowedInWidgetParams: + json[_allowedInWidgetParamsConfigName] as bool? ?? false, + ); +} diff --git a/lib/lints/no_magic_number/no_magic_number_rule.dart b/lib/lints/no_magic_number/no_magic_number_rule.dart new file mode 100644 index 00000000..28d03797 --- /dev/null +++ b/lib/lints/no_magic_number/no_magic_number_rule.dart @@ -0,0 +1,165 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:collection/collection.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/no_magic_number/models/no_magic_number_parameters.dart'; +import 'package:solid_lints/lints/no_magic_number/no_magic_number_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `no_magic_number` rule which forbids having numbers without variable +class NoMagicNumberRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error when having magic number. + static const String lintName = 'no_magic_number'; + + NoMagicNumberRule._(super.config); + + /// Creates a new instance of [NoMagicNumberRule] + /// based on the lint configuration. + factory NoMagicNumberRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + paramsParser: NoMagicNumberParameters.fromJson, + problemMessage: (_) => 'Avoid using magic numbers.' + 'Extract them to named constants or variables.', + ); + + return NoMagicNumberRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = NoMagicNumberVisitor(); + node.accept(visitor); + + final magicNumbers = visitor.literals + .where(_isMagicNumber) + .where(_isNotInsideVariable) + .where(_isNotInsideCollectionLiteral) + .where(_isNotInsideConstMap) + .where(_isNotInsideConstConstructor) + .where(_isNotInDateTime) + .where(_isNotInsideIndexExpression) + .where(_isNotInsideEnumConstantArguments) + .where(_isNotDefaultValue) + .where(_isNotInConstructorInitializer) + .where(_isNotWidgetParameter); + + for (final magicNumber in magicNumbers) { + reporter.reportErrorForNode(code, magicNumber); + } + }); + } + + bool _isMagicNumber(Literal l) => + (l is DoubleLiteral && + !config.parameters.allowedNumbers.contains(l.value)) || + (l is IntegerLiteral && + !config.parameters.allowedNumbers.contains(l.value)); + + bool _isNotInsideVariable(Literal l) => + l.thisOrAncestorMatching( + (ancestor) => ancestor is VariableDeclaration, + ) == + null; + + bool _isNotInDateTime(Literal l) => + l.thisOrAncestorMatching( + (a) => + a is InstanceCreationExpression && + a.staticType?.getDisplayString(withNullability: false) == + 'DateTime', + ) == + null; + + bool _isNotInsideEnumConstantArguments(Literal l) { + final node = l.thisOrAncestorMatching( + (ancestor) => ancestor is EnumConstantArguments, + ); + + return node == null; + } + + bool _isNotInsideCollectionLiteral(Literal l) => l.parent is! TypedLiteral; + + bool _isNotInsideConstMap(Literal l) { + final grandParent = l.parent?.parent; + + return !(grandParent is SetOrMapLiteral && grandParent.isConst); + } + + bool _isNotInsideConstConstructor(Literal l) => + l.thisOrAncestorMatching( + (ancestor) => + ancestor is InstanceCreationExpression && ancestor.isConst, + ) == + null; + + bool _isNotInsideIndexExpression(Literal l) => l.parent is! IndexExpression; + + bool _isNotDefaultValue(Literal literal) { + return literal.thisOrAncestorOfType() == null; + } + + bool _isNotInConstructorInitializer(Literal literal) { + return literal.thisOrAncestorOfType() == null; + } + + bool _isNotWidgetParameter(Literal literal) { + if (!config.parameters.allowedInWidgetParams) return true; + + final widgetCreationExpression = literal.thisOrAncestorMatching( + _isWidgetCreationExpression, + ); + + return widgetCreationExpression == null; + } + + bool _isWidgetCreationExpression( + AstNode node, + ) { + if (node is! InstanceCreationExpression) return false; + + final staticType = node.staticType; + + if (staticType is! InterfaceType) return false; + + final widgetSupertype = staticType.allSupertypes.firstWhereOrNull( + (supertype) => + supertype.getDisplayString(withNullability: false) == 'Widget', + ); + + return widgetSupertype != null; + } +} diff --git a/lib/lints/no_magic_number/no_magic_number_visitor.dart b/lib/lints/no_magic_number/no_magic_number_visitor.dart new file mode 100644 index 00000000..7bec43f6 --- /dev/null +++ b/lib/lints/no_magic_number/no_magic_number_visitor.dart @@ -0,0 +1,45 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// The AST visitor that will find all double and integer literals +class NoMagicNumberVisitor extends RecursiveAstVisitor { + final _literals = []; + + /// List of all double and integer literals + Iterable get literals => _literals; + + @override + void visitDoubleLiteral(DoubleLiteral node) { + _literals.add(node); + super.visitDoubleLiteral(node); + } + + @override + void visitIntegerLiteral(IntegerLiteral node) { + _literals.add(node); + super.visitIntegerLiteral(node); + } +} diff --git a/lib/lints/number_of_parameters/models/number_of_parameters_parameters.dart b/lib/lints/number_of_parameters/models/number_of_parameters_parameters.dart new file mode 100644 index 00000000..5e3485a6 --- /dev/null +++ b/lib/lints/number_of_parameters/models/number_of_parameters_parameters.dart @@ -0,0 +1,19 @@ +/// A data model class that represents the "number of parameters" input +/// parameters. +class NumberOfParametersParameters { + /// Maximum number of parameters + final int maxParameters; + + static const _defaultMaxParameters = 2; + + /// Constructor for [NumberOfParametersParameters] model + const NumberOfParametersParameters({ + required this.maxParameters, + }); + + /// Method for creating from json data + factory NumberOfParametersParameters.fromJson(Map json) => + NumberOfParametersParameters( + maxParameters: json['max_parameters'] as int? ?? _defaultMaxParameters, + ); +} diff --git a/lib/lints/number_of_parameters/number_of_parameters_metric.dart b/lib/lints/number_of_parameters/number_of_parameters_metric.dart new file mode 100644 index 00000000..1df44af5 --- /dev/null +++ b/lib/lints/number_of_parameters/number_of_parameters_metric.dart @@ -0,0 +1,57 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/number_of_parameters/models/number_of_parameters_parameters.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A number of parameters metric which checks whether we didn't exceed +/// the maximum allowed number of parameters for a function or a method +class NumberOfParametersMetric + extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if number of + /// parameters reaches the maximum value. + static const lintName = 'number_of_parameters'; + + NumberOfParametersMetric._(super.rule); + + /// Creates a new instance of [NumberOfParametersMetric] + /// based on the lint configuration. + factory NumberOfParametersMetric.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + configs: configs, + name: lintName, + paramsParser: NumberOfParametersParameters.fromJson, + problemMessage: (value) => + 'The maximum allowed number of parameters is ${value.maxParameters}. ' + 'Try reducing the number of parameters.', + ); + + return NumberOfParametersMetric._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addDeclaration((node) { + final parameters = switch (node) { + (final MethodDeclaration node) => + node.parameters?.parameters.length ?? 0, + (final FunctionDeclaration node) => + node.functionExpression.parameters?.parameters.length ?? 0, + _ => 0, + }; + + if (parameters > config.parameters.maxParameters) { + reporter.reportErrorForOffset( + code, + node.firstTokenAfterCommentAndMetadata.offset, + node.end, + ); + } + }); + } +} diff --git a/lib/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart b/lib/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart new file mode 100644 index 00000000..54d7768e --- /dev/null +++ b/lib/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart @@ -0,0 +1,21 @@ +/// A data model class that represents the "prefer conditional expressions" +/// input parameters. +class PreferConditionalExpressionsParameters { + static const _ignoreNestedConfig = 'ignore_nested'; + + /// Should rule ignore nested if statements + final bool ignoreNested; + + /// Constructor for [PreferConditionalExpressionsParameters] model + const PreferConditionalExpressionsParameters({ + required this.ignoreNested, + }); + + /// Method for creating from json data + factory PreferConditionalExpressionsParameters.fromJson( + Map json, + ) => + PreferConditionalExpressionsParameters( + ignoreNested: json[_ignoreNestedConfig] as bool? ?? false, + ); +} diff --git a/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_fix.dart b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_fix.dart new file mode 100644 index 00000000..49bbfad0 --- /dev/null +++ b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_fix.dart @@ -0,0 +1,117 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_visitor.dart'; + +/// A Quick fix for `prefer_conditional_expressions` rule +/// Suggests to remove unnecessary assertions +class PreferConditionalExpressionsFix extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addIfStatement((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final statementInfo = analysisError.data as StatementInfo?; + if (statementInfo == null) return; + + final correction = _createCorrection(statementInfo); + if (correction == null) return; + + _addReplacement(reporter, statementInfo.statement, correction); + } + }); + } + + void _addReplacement( + ChangeReporter reporter, + IfStatement node, + String correction, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: "Convert to conditional expression.", + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + SourceRange(node.offset, node.length), + correction, + ); + }); + } + + String? _createCorrection(StatementInfo info) { + final thenStatement = info.unwrappedThenStatement; + final elseStatement = info.unwrappedElseStatement; + + final condition = info.statement.expression; + + if (thenStatement is AssignmentExpression && + elseStatement is AssignmentExpression) { + final target = thenStatement.leftHandSide; + final firstExpression = thenStatement.rightHandSide; + final secondExpression = elseStatement.rightHandSide; + + final thenStatementOperator = thenStatement.operator.type; + final elseStatementOperator = elseStatement.operator.type; + + if (_isAssignmentOperatorNotEq(thenStatementOperator) && + _isAssignmentOperatorNotEq(elseStatementOperator)) { + final prefix = thenStatement.leftHandSide; + final thenPart = + '$prefix ${thenStatementOperator.stringValue} $firstExpression'; + final elsePart = + '$prefix ${elseStatementOperator.stringValue} $secondExpression;'; + + return '$condition ? $thenPart : $elsePart'; + } + + final correctionForLiterals = _createCorrectionForLiterals( + condition, + firstExpression, + secondExpression, + ); + + return '$target = $correctionForLiterals'; + } + + if (thenStatement is ReturnStatement && elseStatement is ReturnStatement) { + final firstExpression = thenStatement.expression; + final secondExpression = elseStatement.expression; + final correction = _createCorrectionForLiterals( + condition, + firstExpression, + secondExpression, + ); + + return 'return $correction'; + } + + return null; + } + + String _createCorrectionForLiterals( + Expression condition, + Expression? firstExpression, + Expression? secondExpression, + ) { + if (firstExpression is BooleanLiteral && + secondExpression is BooleanLiteral) { + final isInverted = !firstExpression.value && secondExpression.value; + + return '${isInverted ? "!" : ""}$condition;'; + } + + return '$condition ? $firstExpression : $secondExpression;'; + } + + bool _isAssignmentOperatorNotEq(TokenType token) => + token.isAssignmentOperator && token != TokenType.EQ; +} diff --git a/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart new file mode 100644 index 00000000..eb36a7c4 --- /dev/null +++ b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart @@ -0,0 +1,62 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart'; +import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_fix.dart'; +import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +// Inspired by TSLint (https://palantir.github.io/tslint/rules/prefer-conditional-expression/) + +/// A `prefer_conditional_expressions` rule which warns about +/// simple if statements that can be replaced with conditional expressions +class PreferConditionalExpressionsRule + extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if number of + /// parameters reaches the maximum value. + static const lintName = 'prefer_conditional_expressions'; + + PreferConditionalExpressionsRule._(super.config); + + /// Creates a new instance of [PreferConditionalExpressionsRule] + /// based on the lint configuration. + factory PreferConditionalExpressionsRule.createRule( + CustomLintConfigs configs, + ) { + final config = RuleConfig( + configs: configs, + name: lintName, + paramsParser: PreferConditionalExpressionsParameters.fromJson, + problemMessage: (value) => 'Prefer conditional expression.', + ); + + return PreferConditionalExpressionsRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = PreferConditionalExpressionsVisitor( + ignoreNested: config.parameters.ignoreNested, + ); + node.accept(visitor); + + for (final element in visitor.statementsInfo) { + reporter.reportErrorForNode( + code, + element.statement, + null, + null, + element, + ); + } + }); + } + + @override + List getFixes() => [PreferConditionalExpressionsFix()]; +} diff --git a/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_visitor.dart b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_visitor.dart new file mode 100644 index 00000000..c4df3ff4 --- /dev/null +++ b/lib/lints/prefer_conditional_expressions/prefer_conditional_expressions_visitor.dart @@ -0,0 +1,158 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// The AST visitor that will collect all if statements that can be simplified +/// into conditional expressions. +class PreferConditionalExpressionsVisitor extends RecursiveAstVisitor { + final _statementsInfo = []; + + final bool _ignoreNested; + + /// List of statement info that represents all simple if statements + Iterable get statementsInfo => _statementsInfo; + + /// Creates instance of [PreferConditionalExpressionsVisitor] + PreferConditionalExpressionsVisitor({ + required bool ignoreNested, + }) : _ignoreNested = ignoreNested; + + @override + void visitIfStatement(IfStatement node) { + super.visitIfStatement(node); + + if (_ignoreNested) { + final visitor = _ConditionalsVisitor(); + node.visitChildren(visitor); + + if (visitor.hasInnerConditionals) { + return; + } + } + + if (node.parent is! IfStatement && + node.elseStatement != null && + node.elseStatement is! IfStatement) { + _checkBothAssignment(node); + _checkBothReturn(node); + } + } + + void _checkBothAssignment(IfStatement statement) { + final thenAssignment = _getAssignmentExpression(statement.thenStatement); + final elseAssignment = _getAssignmentExpression(statement.elseStatement); + + if (thenAssignment != null && + elseAssignment != null && + _haveEqualNames(thenAssignment, elseAssignment)) { + _statementsInfo.add( + StatementInfo( + statement: statement, + unwrappedThenStatement: thenAssignment, + unwrappedElseStatement: elseAssignment, + ), + ); + } + } + + AssignmentExpression? _getAssignmentExpression(Statement? statement) { + if (statement is ExpressionStatement && + statement.expression is AssignmentExpression) { + return statement.expression as AssignmentExpression; + } + + if (statement is Block && statement.statements.length == 1) { + return _getAssignmentExpression(statement.statements.first); + } + + return null; + } + + bool _haveEqualNames( + AssignmentExpression thenAssignment, + AssignmentExpression elseAssignment, + ) => + thenAssignment.leftHandSide is Identifier && + elseAssignment.leftHandSide is Identifier && + (thenAssignment.leftHandSide as Identifier).name == + (elseAssignment.leftHandSide as Identifier).name; + + void _checkBothReturn(IfStatement statement) { + final thenReturn = _getReturnStatement(statement.thenStatement); + final elseReturn = _getReturnStatement(statement.elseStatement); + + if (thenReturn != null && elseReturn != null) { + _statementsInfo.add( + StatementInfo( + statement: statement, + unwrappedThenStatement: thenReturn, + unwrappedElseStatement: elseReturn, + ), + ); + } + } + + ReturnStatement? _getReturnStatement(Statement? statement) { + if (statement is ReturnStatement) { + return statement; + } + + if (statement is Block && statement.statements.length == 1) { + return _getReturnStatement(statement.statements.first); + } + + return null; + } +} + +class _ConditionalsVisitor extends RecursiveAstVisitor { + bool hasInnerConditionals = false; + + @override + void visitConditionalExpression(ConditionalExpression node) { + hasInnerConditionals = true; + + super.visitConditionalExpression(node); + } +} + +/// Data class contains info required for fix +class StatementInfo { + /// If statement node + final IfStatement statement; + + /// Contents of if block + final AstNode unwrappedThenStatement; + + /// Contents of else block + final AstNode unwrappedElseStatement; + + /// Creates instance of an [StatementInfo] + const StatementInfo({ + required this.statement, + required this.unwrappedThenStatement, + required this.unwrappedElseStatement, + }); +} diff --git a/lib/lints/prefer_first/prefer_first_fix.dart b/lib/lints/prefer_first/prefer_first_fix.dart new file mode 100644 index 00000000..e52ba9ce --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_fix.dart @@ -0,0 +1,67 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// A Quick fix for `prefer_first` rule +/// Suggests to replace iterable access expressions +class PreferFirstFix extends DartFix { + static const _replaceComment = "Replace with 'first'."; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addMethodInvocation((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + + context.registry.addIndexExpression((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + } + + String _createCorrection(Expression expression) { + if (expression is MethodInvocation) { + return expression.isCascaded + ? '..first' + : '${expression.target ?? ''}.first'; + } else if (expression is IndexExpression) { + return expression.isCascaded + ? '..first' + : '${expression.target ?? ''}.first'; + } else { + return '.first'; + } + } + + void _addReplacement( + ChangeReporter reporter, + Expression node, + String correction, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: _replaceComment, + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + SourceRange(node.offset, node.length), + correction, + ); + }); + } +} diff --git a/lib/lints/prefer_first/prefer_first_rule.dart b/lib/lints/prefer_first/prefer_first_rule.dart new file mode 100644 index 00000000..fff405ff --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_rule.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_fix.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `prefer_first` rule which warns about +/// usage of iterable[0] or iterable.elementAt(0) +class PreferFirstRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if number of + /// parameters reaches the maximum value. + static const lintName = 'prefer_first'; + + PreferFirstRule._(super.config); + + /// Creates a new instance of [PreferFirstRule] + /// based on the lint configuration. + factory PreferFirstRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => + 'Use first instead of accessing the element at zero index.', + ); + + return PreferFirstRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = PreferFirstVisitor(); + node.accept(visitor); + + for (final element in visitor.expressions) { + reporter.reportErrorForNode(code, element); + } + }); + } + + @override + List getFixes() => [PreferFirstFix()]; +} diff --git a/lib/lints/prefer_first/prefer_first_visitor.dart b/lib/lints/prefer_first/prefer_first_visitor.dart new file mode 100644 index 00000000..3abdf1e5 --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_visitor.dart @@ -0,0 +1,40 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// The AST visitor that will collect all Iterable access expressions +/// which can be replaced with .first +class PreferFirstVisitor extends RecursiveAstVisitor { + final _expressions = []; + + /// List of all Iterable access expressions + Iterable get expressions => _expressions; + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + final isIterable = isIterableOrSubclass(node.realTarget?.staticType); + final isElementAt = node.methodName.name == 'elementAt'; + + if (isIterable && isElementAt) { + final arg = node.argumentList.arguments.first; + + if (arg is IntegerLiteral && arg.value == 0) { + _expressions.add(node); + } + } + } + + @override + void visitIndexExpression(IndexExpression node) { + super.visitIndexExpression(node); + + if (isListOrSubclass(node.realTarget.staticType)) { + final index = node.index; + + if (index is IntegerLiteral && index.value == 0) { + _expressions.add(node); + } + } + } +} diff --git a/lib/lints/prefer_last/prefer_last_fix.dart b/lib/lints/prefer_last/prefer_last_fix.dart new file mode 100644 index 00000000..88f236e8 --- /dev/null +++ b/lib/lints/prefer_last/prefer_last_fix.dart @@ -0,0 +1,67 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// A Quick fix for `prefer_last` rule +/// Suggests to replace iterable access expressions +class PreferLastFix extends DartFix { + static const _replaceComment = "Replace with 'last'."; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addMethodInvocation((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + + context.registry.addIndexExpression((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + } + + String _createCorrection(Expression expression) { + if (expression is MethodInvocation) { + return expression.isCascaded + ? '..last' + : '${expression.target ?? ''}.last'; + } else if (expression is IndexExpression) { + return expression.isCascaded + ? '..last' + : '${expression.target ?? ''}.last'; + } else { + return '.last'; + } + } + + void _addReplacement( + ChangeReporter reporter, + Expression node, + String correction, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: _replaceComment, + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + SourceRange(node.offset, node.length), + correction, + ); + }); + } +} diff --git a/lib/lints/prefer_last/prefer_last_rule.dart b/lib/lints/prefer_last/prefer_last_rule.dart new file mode 100644 index 00000000..75254cb2 --- /dev/null +++ b/lib/lints/prefer_last/prefer_last_rule.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/prefer_last/prefer_last_fix.dart'; +import 'package:solid_lints/lints/prefer_last/prefer_last_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `prefer_last` rule which warns about +/// usage of iterable[length-1] or iterable.elementAt(length-1) +class PreferLastRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if iterable + /// access can be simplified. + static const lintName = 'prefer_last'; + + PreferLastRule._(super.config); + + /// Creates a new instance of [PreferLastRule] + /// based on the lint configuration. + factory PreferLastRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => + 'Use last instead of accessing the last element by index.', + ); + + return PreferLastRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = PreferLastVisitor(); + node.accept(visitor); + + for (final element in visitor.expressions) { + reporter.reportErrorForNode(code, element); + } + }); + } + + @override + List getFixes() => [PreferLastFix()]; +} diff --git a/lib/lints/prefer_last/prefer_last_visitor.dart b/lib/lints/prefer_last/prefer_last_visitor.dart new file mode 100644 index 00000000..aa76127f --- /dev/null +++ b/lib/lints/prefer_last/prefer_last_visitor.dart @@ -0,0 +1,74 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// The AST visitor that will collect all Iterable access expressions +/// which can be replaced with .last +class PreferLastVisitor extends RecursiveAstVisitor { + final _expressions = []; + + /// List of all Iterable access expressions + Iterable get expressions => _expressions; + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + + final target = node.realTarget; + + if (isIterableOrSubclass(target?.staticType) && + node.methodName.name == 'elementAt') { + final arg = node.argumentList.arguments.first; + + if (arg is BinaryExpression && + _isLastElementAccess(arg, target.toString())) { + _expressions.add(node); + } + } + } + + @override + void visitIndexExpression(IndexExpression node) { + super.visitIndexExpression(node); + + final target = node.realTarget; + + if (isListOrSubclass(target.staticType)) { + final index = node.index; + + if (index is BinaryExpression && + _isLastElementAccess(index, target.toString())) { + _expressions.add(node); + } + } + } + + bool _isLastElementAccess(BinaryExpression expression, String targetName) { + final left = expression.leftOperand; + final right = expression.rightOperand; + final leftName = _getLeftOperandName(left); + + if (right is! IntegerLiteral) return false; + if (right.value != 1) return false; + if (expression.operator.type != TokenType.MINUS) return false; + + return leftName == '$targetName.length'; + } + + String? _getLeftOperandName(Expression expression) { + if (expression is PrefixedIdentifier) { + return expression.name; + } + + /// Access target like map.keys.length is being reported as PropertyAccess + /// expression this case will handle such cases + if (expression is PropertyAccess) { + if (expression.operator.type != TokenType.PERIOD) return null; + + return expression.toString(); + } + + return null; + } +} diff --git a/lib/lints/prefer_match_file_name/models/declaration_token_info.dart b/lib/lints/prefer_match_file_name/models/declaration_token_info.dart new file mode 100644 index 00000000..bfceb8e0 --- /dev/null +++ b/lib/lints/prefer_match_file_name/models/declaration_token_info.dart @@ -0,0 +1,11 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; + +/// Data class represents declaration token and declaration parent node +typedef DeclarationTokenInfo = ({ + /// Declaration token + Token token, + + /// Declaration parent node + AstNode parent, +}); diff --git a/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart b/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart new file mode 100644 index 00000000..80817b6e --- /dev/null +++ b/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:path/path.dart' as p; +import 'package:solid_lints/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; +import 'package:solid_lints/utils/node_utils.dart'; + +/// A `prefer_match_file_name` rule which warns about +/// mismatch between file name and declared element inside +class PreferMatchFileNameRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if iterable + /// access can be simplified. + static const String lintName = 'prefer_match_file_name'; + static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]'); + + PreferMatchFileNameRule._(super.config); + + /// Creates a new instance of [PreferMatchFileNameRule] + /// based on the lint configuration. + factory PreferMatchFileNameRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => + 'File name does not match with first declared element name.', + ); + + return PreferMatchFileNameRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = PreferMatchFileNameVisitor(); + + node.accept(visitor); + + if (visitor.declarations.isEmpty) return; + + final firstDeclaration = visitor.declarations.first; + + if (_doNormalizedNamesMatch( + resolver.source.fullName, + firstDeclaration.token.lexeme, + )) return; + + final nodeType = + humanReadableNodeType(firstDeclaration.parent).toLowerCase(); + + reporter.reportErrorForToken( + LintCode( + name: lintName, + problemMessage: 'File name does not match with first $nodeType name.', + ), + firstDeclaration.token, + ); + }); + } + + bool _doNormalizedNamesMatch(String path, String identifierName) { + final fileName = _normalizePath(path); + final dartIdentifier = _normalizeDartIdentifierName(identifierName); + + return fileName == dartIdentifier; + } + + String _normalizePath(String s) => p + .basename(s) + .split('.') + .first + .replaceAll(_onlySymbolsRegex, '') + .toLowerCase(); + + String _normalizeDartIdentifierName(String s) => + s.replaceAll(_onlySymbolsRegex, '').toLowerCase(); +} diff --git a/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart b/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart new file mode 100644 index 00000000..f3e95aa9 --- /dev/null +++ b/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart @@ -0,0 +1,68 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/lints/prefer_match_file_name/models/declaration_token_info.dart'; + +/// The AST visitor that will collect all Class, Enum, Extension and Mixin +/// declarations +class PreferMatchFileNameVisitor extends RecursiveAstVisitor { + final _declarations = []; + + /// List of all declarations + Iterable get declarations => _declarations + ..sort( + // partition into public and private + // put public ones first + // each partition sorted by declaration order + (a, b) => _publicDeclarationsFirst(a, b) ?? _byDeclarationOrder(a, b), + ); + + @override + void visitClassDeclaration(ClassDeclaration node) { + super.visitClassDeclaration(node); + + _declarations.add((token: node.name, parent: node)); + } + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) { + super.visitExtensionDeclaration(node); + + final name = node.name; + if (name != null) { + _declarations.add((token: name, parent: node)); + } + } + + @override + void visitMixinDeclaration(MixinDeclaration node) { + super.visitMixinDeclaration(node); + + _declarations.add((token: node.name, parent: node)); + } + + @override + void visitEnumDeclaration(EnumDeclaration node) { + super.visitEnumDeclaration(node); + + _declarations.add((token: node.name, parent: node)); + } + + int? _publicDeclarationsFirst( + DeclarationTokenInfo a, + DeclarationTokenInfo b, + ) { + final isAPrivate = Identifier.isPrivateName(a.token.lexeme); + final isBPrivate = Identifier.isPrivateName(b.token.lexeme); + if (!isAPrivate && isBPrivate) { + return -1; + } else if (isAPrivate && !isBPrivate) { + return 1; + } + // no reorder needed; + return null; + } + + int _byDeclarationOrder(DeclarationTokenInfo a, DeclarationTokenInfo b) { + return a.token.offset.compareTo(b.token.offset); + } +} diff --git a/lib/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/lints/proper_super_calls/proper_super_calls_rule.dart new file mode 100644 index 00000000..0096c346 --- /dev/null +++ b/lib/lints/proper_super_calls/proper_super_calls_rule.dart @@ -0,0 +1,134 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// Checks that `super` calls in the initState and +/// dispose methods are called in the correct order. + +class ProperSuperCallsRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents + /// the error whether the initState and dispose methods + /// are called in the incorrect order + static const lintName = 'proper_super_calls'; + static const _initState = 'initState'; + static const _dispose = 'dispose'; + static const _override = 'override'; + + /// The [LintCode] of this lint rule that represents + /// the error whether super.initState() should be called first + static const _superInitStateCode = LintCode( + name: lintName, + problemMessage: "super.initState() should be first", + ); + + /// The [LintCode] of this lint rule that represents + /// the error whether super.dispose() should be called last + static const _superDisposeCode = LintCode( + name: lintName, + problemMessage: "super.dispose() should be last", + ); + + ProperSuperCallsRule._(super.config); + + /// Creates a new instance of [ProperSuperCallsRule] + /// based on the lint configuration. + factory ProperSuperCallsRule.createRule(CustomLintConfigs configs) { + final rule = RuleConfig( + name: lintName, + configs: configs, + problemMessage: (_) => 'Proper super calls issue', + ); + + return ProperSuperCallsRule._(rule); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addMethodDeclaration( + (node) { + final methodName = node.name.toString(); + + if (methodName == _initState || methodName == _dispose) { + final statements = (node.body as BlockFunctionBody).block.statements; + + _checkSuperCalls( + node, + methodName, + statements, + reporter, + ); + } + }, + ); + } + + /// This method report an error whether `super.initState()` + /// or `super.dispose()` are called incorrect + void _checkSuperCalls( + MethodDeclaration node, + String methodName, + List statements, + ErrorReporter reporter, + ) { + final hasOverrideAnnotation = + node.metadata.any((annotation) => annotation.name.name == _override); + + if (!hasOverrideAnnotation) return; + if (methodName == _initState && !_isSuperInitStateCalledFirst(statements)) { + reporter.reportErrorForNode( + _superInitStateCode, + node, + ); + } + if (methodName == _dispose && !_isSuperDisposeCalledLast(statements)) { + reporter.reportErrorForNode( + _superDisposeCode, + node, + ); + } + } + + /// Returns `true` if `super.initState()` is called before other code in the + /// `initState` method, `false` otherwise. + bool _isSuperInitStateCalledFirst(List statements) { + if (statements.isEmpty) return false; + final firstStatement = statements.first; + + if (firstStatement is ExpressionStatement) { + final expression = firstStatement.expression; + + final isSuperInitStateCalledFirst = expression is MethodInvocation && + expression.target is SuperExpression && + expression.methodName.toString() == _initState; + + return isSuperInitStateCalledFirst; + } + + return false; + } + + /// Returns `true` if `super.dispose()` is called at the end of the `dispose` + /// method, `false` otherwise. + bool _isSuperDisposeCalledLast(List statements) { + if (statements.isEmpty) return false; + final lastStatement = statements.last; + + if (lastStatement is ExpressionStatement) { + final expression = lastStatement.expression; + + final lastStatementIsSuperDispose = expression is MethodInvocation && + expression.target is SuperExpression && + expression.methodName.toString() == _dispose; + + return lastStatementIsSuperDispose; + } + + return false; + } +} diff --git a/lib/models/rule_config.dart b/lib/models/rule_config.dart new file mode 100644 index 00000000..d2429b9d --- /dev/null +++ b/lib/models/rule_config.dart @@ -0,0 +1,43 @@ +import 'package:analyzer/error/error.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// Type definition of a value factory which allows us to map data from +/// YAML configuration to an object of type [T]. +typedef RuleParameterParser = T Function(Map json); + +/// Type definition for a problem message factory after finding a problem +/// by a given lint. +typedef RuleProblemFactory = String Function(T value); + +/// [RuleConfig] allows us to quickly parse a lint rule and +/// declare basic configuration for it. +class RuleConfig { + /// Constructor for [RuleConfig] model. + RuleConfig({ + required this.name, + required CustomLintConfigs configs, + required RuleProblemFactory problemMessage, + RuleParameterParser? paramsParser, + }) : enabled = configs.rules[name]?.enabled ?? false, + parameters = paramsParser?.call(configs.rules[name]?.json ?? {}) as T, + _problemMessageFactory = problemMessage; + + /// The [LintCode] of this lint rule that represents the error. + final String name; + + /// A flag which indicates whether this rule was enabled by the user. + final bool enabled; + + /// Value with a configuration for a particular rule. + final T parameters; + + /// Factory for generating error messages. + final RuleProblemFactory _problemMessageFactory; + + /// [LintCode] which is generated based on the provided data. + LintCode get lintCode => LintCode( + name: name, + problemMessage: _problemMessageFactory(parameters), + errorSeverity: ErrorSeverity.WARNING, + ); +} diff --git a/lib/models/solid_lint_rule.dart b/lib/models/solid_lint_rule.dart new file mode 100644 index 00000000..62a95750 --- /dev/null +++ b/lib/models/solid_lint_rule.dart @@ -0,0 +1,16 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/models/rule_config.dart'; + +/// A base class for emitting information about +/// issues with user's `.dart` files. +abstract class SolidLintRule extends DartLintRule { + /// Constructor for [SolidLintRule] model. + SolidLintRule(this.config) : super(code: config.lintCode); + + /// Configuration for a particular rule with all the + /// defined custom parameters. + final RuleConfig config; + + /// A flag which indicates whether this rule was enabled by the user. + bool get enabled => config.enabled; +} diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart new file mode 100644 index 00000000..2282a656 --- /dev/null +++ b/lib/solid_lints.dart @@ -0,0 +1,66 @@ +library solid_metrics; + +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/avoid_global_state/avoid_global_state_rule.dart'; +import 'package:solid_lints/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; +import 'package:solid_lints/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; +import 'package:solid_lints/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; +import 'package:solid_lints/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; +import 'package:solid_lints/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; +import 'package:solid_lints/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart'; +import 'package:solid_lints/lints/double_literal_format/double_literal_format_rule.dart'; +import 'package:solid_lints/lints/function_lines_of_code/function_lines_of_code_metric.dart'; +import 'package:solid_lints/lints/member_ordering/member_ordering_rule.dart'; +import 'package:solid_lints/lints/newline_before_return/newline_before_return_rule.dart'; +import 'package:solid_lints/lints/no_empty_block/no_empty_block_rule.dart'; +import 'package:solid_lints/lints/no_equal_then_else/no_equal_then_else_rule.dart'; +import 'package:solid_lints/lints/no_magic_number/no_magic_number_rule.dart'; +import 'package:solid_lints/lints/number_of_parameters/number_of_parameters_metric.dart'; +import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_rule.dart'; +import 'package:solid_lints/lints/prefer_last/prefer_last_rule.dart'; +import 'package:solid_lints/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; +import 'package:solid_lints/lints/proper_super_calls/proper_super_calls_rule.dart'; + +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// Creates a plugin for our custom linter +PluginBase createPlugin() => _SolidLints(); + +/// Initialize custom solid lints +class _SolidLints extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) { + final List supportedRules = [ + CyclomaticComplexityMetric.createRule(configs), + NumberOfParametersMetric.createRule(configs), + FunctionLinesOfCodeMetric.createRule(configs), + AvoidNonNullAssertionRule.createRule(configs), + AvoidLateKeywordRule.createRule(configs), + AvoidGlobalStateRule.createRule(configs), + AvoidReturningWidgetsRule.createRule(configs), + DoubleLiteralFormatRule.createRule(configs), + AvoidUnnecessaryTypeAssertions.createRule(configs), + AvoidUnnecessarySetStateRule.createRule(configs), + AvoidUnnecessaryTypeCastsRule.createRule(configs), + AvoidUnrelatedTypeAssertionsRule.createRule(configs), + AvoidUnusedParametersRule.createRule(configs), + NewlineBeforeReturnRule.createRule(configs), + NoEmptyBlockRule.createRule(configs), + NoEqualThenElseRule.createRule(configs), + MemberOrderingRule.createRule(configs), + NoMagicNumberRule.createRule(configs), + PreferConditionalExpressionsRule.createRule(configs), + PreferFirstRule.createRule(configs), + PreferLastRule.createRule(configs), + PreferMatchFileNameRule.createRule(configs), + ProperSuperCallsRule.createRule(configs), + ]; + + // Return only enabled rules + return supportedRules.where((r) => r.enabled).toList(); + } +} diff --git a/lib/utils/node_utils.dart b/lib/utils/node_utils.dart new file mode 100644 index 00000000..3cea0c01 --- /dev/null +++ b/lib/utils/node_utils.dart @@ -0,0 +1,24 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; + +/// Check node is override method from its metadata +bool isOverride(List metadata) => metadata.any( + (node) => + node.name.name == 'override' && node.atSign.type == TokenType.AT, + ); + +/// Returns human readable node type +/// Self explanatory +String humanReadableNodeType(AstNode? node) { + if (node is ClassDeclaration) { + return 'Class'; + } else if (node is EnumDeclaration) { + return 'Enum'; + } else if (node is ExtensionDeclaration) { + return 'Extension'; + } else if (node is MixinDeclaration) { + return 'Mixin'; + } + + return 'Node'; +} diff --git a/lib/utils/parameter_utils.dart b/lib/utils/parameter_utils.dart new file mode 100644 index 00000000..2fc20b8e --- /dev/null +++ b/lib/utils/parameter_utils.dart @@ -0,0 +1,10 @@ +import 'package:analyzer/dart/ast/ast.dart'; + +/// Checks if parameter name consists only of underscores +bool nameConsistsOfUnderscoresOnly(FormalParameter parameter) { + final paramName = parameter.name; + + if (paramName == null) return false; + + return paramName.lexeme.replaceAll('_', '').isEmpty; +} diff --git a/lib/utils/typecast_utils.dart b/lib/utils/typecast_utils.dart new file mode 100644 index 00000000..be228d16 --- /dev/null +++ b/lib/utils/typecast_utils.dart @@ -0,0 +1,104 @@ +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +// Useful types for testing expressions' types + +// TODO: consider to move here common functions for rules +// avoid_unnecessary_type_assertions +// avoid_unnecessary_type_casts (after implementation) +// avoid_unrelated_type_assertions (after implementation) + +/// A class for representing arguments for types checking methods +class TypeCast { + /// The static type ot the tested expression + final DartType source; + + /// The type being tested for + final DartType target; + + /// Set to true for opposite comparison, i.e 'is!' + final bool isReversed; + + /// Creates a new Typecast object with a given expression's or object's type + /// and a tested type + TypeCast({ + required this.source, + required this.target, + this.isReversed = false, + }); + + /// Returns the first type from source's supertypes + /// which is corresponding to target or null + DartType? castTypeInHierarchy() { + if (source.element == target.element) { + return source; + } + + final objectType = source; + if (objectType is InterfaceType) { + return objectType.allSupertypes.firstWhereOrNull( + (value) => value.element == target.element, + ); + } + + return null; + } + + /// Checks for nullable type casts + /// Only one case `Type? is Type` always valid assertion case. + bool get isNullableCompatibility { + final isObjectTypeNullable = + source.nullabilitySuffix != NullabilitySuffix.none; + final isCastedTypeNullable = + target.nullabilitySuffix != NullabilitySuffix.none; + + return isObjectTypeNullable && !isCastedTypeNullable; + } + + /// Checks that type checking is unnecessary + /// [source] is the source expression type + /// [target] is the type against which the expression type is compared + /// and false for positive comparison, i.e. 'is', 'as' or 'whereType' + bool get isUnnecessaryTypeCheck { + if (isNullableCompatibility) { + return false; + } + + final objectCastedType = castTypeInHierarchy(); + if (objectCastedType == null) { + return isReversed; + } + + final objectTypeCast = TypeCast( + source: objectCastedType, + target: target, + ); + if (!objectTypeCast.areGenericsWithSameTypeArgs) { + return false; + } + + return !isReversed; + } + + /// Checks for type arguments and compares them + bool get areGenericsWithSameTypeArgs { + if (source is DynamicType || target is DynamicType) { + return false; + } + + if (this case TypeCast(source: final objectType, target: final castedType) + when objectType is ParameterizedType && + castedType is ParameterizedType) { + if (objectType.typeArguments.length != castedType.typeArguments.length) { + return false; + } + + return IterableZip([objectType.typeArguments, castedType.typeArguments]) + .map((e) => TypeCast(source: e[0], target: e[1])) + .every((cast) => cast.isUnnecessaryTypeCheck); + } else { + return false; + } + } +} diff --git a/lib/utils/types_utils.dart b/lib/utils/types_utils.dart new file mode 100644 index 00000000..9d42947c --- /dev/null +++ b/lib/utils/types_utils.dart @@ -0,0 +1,179 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// ignore_for_file: public_member_api_docs + +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; + +extension Subtypes on DartType { + Iterable get supertypes { + final element = this.element; + return element is InterfaceElement ? element.allSupertypes : []; + } +} + +bool hasWidgetType(DartType type) => + (isWidgetOrSubclass(type) || + _isIterable(type) || + _isList(type) || + _isFuture(type)) && + !(_isMultiProvider(type) || + _isSubclassOfInheritedProvider(type) || + _isIterableInheritedProvider(type) || + _isListInheritedProvider(type) || + _isFutureInheritedProvider(type)); + +bool isIterable(DartType? type) => + _checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false); + +bool isNullableType(DartType? type) => + type?.nullabilitySuffix == NullabilitySuffix.question; + +bool isWidgetOrSubclass(DartType? type) => + _isWidget(type) || _isSubclassOfWidget(type); + +bool isRenderObjectOrSubclass(DartType? type) => + _isRenderObject(type) || _isSubclassOfRenderObject(type); + +bool isRenderObjectWidgetOrSubclass(DartType? type) => + _isRenderObjectWidget(type) || _isSubclassOfRenderObjectWidget(type); + +bool isRenderObjectElementOrSubclass(DartType? type) => + _isRenderObjectElement(type) || _isSubclassOfRenderObjectElement(type); + +bool isWidgetStateOrSubclass(DartType? type) => + _isWidgetState(type) || _isSubclassOfWidgetState(type); + +bool isSubclassOfListenable(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isListenable); + +bool isListViewWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'ListView'; + +bool isSingleChildScrollViewWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'SingleChildScrollView'; + +bool isColumnWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'Column'; + +bool isRowWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'Row'; + +bool isPaddingWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'Padding'; + +bool isBuildContext(DartType? type) => + type?.getDisplayString(withNullability: false) == 'BuildContext'; + +bool isGameWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'GameWidget'; + +bool _checkSelfOrSupertypes( + DartType? type, + bool Function(DartType?) predicate, +) => + predicate(type) || + (type is InterfaceType && type.allSupertypes.any(predicate)); + +bool _isWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'Widget'; + +bool _isSubclassOfWidget(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isWidget); + +// ignore: deprecated_member_use +bool _isWidgetState(DartType? type) => type?.element2?.displayName == 'State'; + +bool _isSubclassOfWidgetState(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isWidgetState); + +bool _isIterable(DartType type) => + type.isDartCoreIterable && + type is InterfaceType && + isWidgetOrSubclass(type.typeArguments.firstOrNull); + +bool _isList(DartType type) => + type.isDartCoreList && + type is InterfaceType && + isWidgetOrSubclass(type.typeArguments.firstOrNull); + +bool _isFuture(DartType type) => + type.isDartAsyncFuture && + type is InterfaceType && + isWidgetOrSubclass(type.typeArguments.firstOrNull); + +bool _isListenable(DartType type) => + type.getDisplayString(withNullability: false) == 'Listenable'; + +bool _isRenderObject(DartType? type) => + type?.getDisplayString(withNullability: false) == 'RenderObject'; + +bool _isSubclassOfRenderObject(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isRenderObject); + +bool _isRenderObjectWidget(DartType? type) => + type?.getDisplayString(withNullability: false) == 'RenderObjectWidget'; + +bool _isSubclassOfRenderObjectWidget(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isRenderObjectWidget); + +bool _isRenderObjectElement(DartType? type) => + type?.getDisplayString(withNullability: false) == 'RenderObjectElement'; + +bool _isSubclassOfRenderObjectElement(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isRenderObjectElement); + +bool _isMultiProvider(DartType? type) => + type?.getDisplayString(withNullability: false) == 'MultiProvider'; + +bool _isSubclassOfInheritedProvider(DartType? type) => + type is InterfaceType && type.allSupertypes.any(_isInheritedProvider); + +bool _isInheritedProvider(DartType? type) => + type != null && + type + .getDisplayString(withNullability: false) + .startsWith('InheritedProvider<'); + +bool _isIterableInheritedProvider(DartType type) => + type.isDartCoreIterable && + type is InterfaceType && + _isSubclassOfInheritedProvider(type.typeArguments.firstOrNull); + +bool _isListInheritedProvider(DartType type) => + type.isDartCoreList && + type is InterfaceType && + _isSubclassOfInheritedProvider(type.typeArguments.firstOrNull); + +bool _isFutureInheritedProvider(DartType type) => + type.isDartAsyncFuture && + type is InterfaceType && + _isSubclassOfInheritedProvider(type.typeArguments.firstOrNull); + +bool isIterableOrSubclass(DartType? type) => + _checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false); + +bool isListOrSubclass(DartType? type) => + _checkSelfOrSupertypes(type, (t) => t?.isDartCoreList ?? false); diff --git a/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart b/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart new file mode 100644 index 00000000..bd7522a7 --- /dev/null +++ b/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: unused_field +// ignore_for_file: unused_element + +/// Check the `member_ordering` rule +/// alphabetical-by-type option enabled + +class CorrectAlphabeticalByTypeClass { + final double e = 1; + final int a = 1; +} + +class WrongAlphabeticalByTypeClass { + final int a = 1; + + // expect_lint: member_ordering + final double e = 1; +} + +class PartiallyWrongAlphabeticalByTypeClass { + final int a = 1; + final String str = 's'; + + // expect_lint: member_ordering + final double e = 1; +} diff --git a/lint_test/alphabetize_by_type_test/analysis_options.yaml b/lint_test/alphabetize_by_type_test/analysis_options.yaml new file mode 100644 index 00000000..000cfecb --- /dev/null +++ b/lint_test/alphabetize_by_type_test/analysis_options.yaml @@ -0,0 +1,8 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - member_ordering: + alphabetize_by_type: true diff --git a/lint_test/alphabetize_by_type_test/pubspec.yaml b/lint_test/alphabetize_by_type_test/pubspec.yaml new file mode 100644 index 00000000..fb785ca7 --- /dev/null +++ b/lint_test/alphabetize_by_type_test/pubspec.yaml @@ -0,0 +1,14 @@ +name: solid_lints_alphabetize_by_type_test +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + solid_lints: + path: ../../ + test: ^1.20.1 diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml new file mode 100644 index 00000000..c644d4d1 --- /dev/null +++ b/lint_test/analysis_options.yaml @@ -0,0 +1,60 @@ +analyzer: + plugins: + - custom_lint + +custom_lint: + rules: + - cyclomatic_complexity: + max_complexity: 4 + - number_of_parameters: + max_parameters: 2 + - function_lines_of_code: + max_lines: 50 + - avoid_non_null_assertion + - avoid_late_keyword: + allow_initialized: true + ignored_types: + - ColorTween + - AnimationController + - avoid_global_state + - avoid_returning_widgets + - avoid_unnecessary_setstate + - double_literal_format + - avoid_unnecessary_type_assertions + - avoid_unnecessary_type_casts + - avoid_unrelated_type_assertions + - avoid_unused_parameters + - newline_before_return + - no_empty_block + - no_equal_then_else + - member_ordering: + alphabetize: true + order: + - public_fields + - private_fields + - constructors + - getters + - setters + - public_methods + - private_methods + - close_method + widgets_order: + - const_fields + - static_fields + - static_methods + - public_fields + - private_fields + - public_methods + - private_methods + - constructors + - build_method + - init_state_method + - did_change_dependencies_method + - did_update_widget_method + - dispose_method + - no_magic_number + - prefer_conditional_expressions + - prefer_first + - prefer_last + - prefer_match_file_name + - proper_super_calls diff --git a/lint_test/avoid_global_state_test.dart b/lint_test/avoid_global_state_test.dart new file mode 100644 index 00000000..e790523e --- /dev/null +++ b/lint_test/avoid_global_state_test.dart @@ -0,0 +1,12 @@ +// ignore_for_file: type_annotate_public_apis, prefer_match_file_name + +/// Check global mutable variable fail +/// `avoid_global_state` + +// expect_lint: avoid_global_state +var globalMutable = 0; + +class Test { + // expect_lint: avoid_global_state + static int globalMutable = 0; +} diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml b/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml new file mode 100644 index 00000000..7f92efe8 --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/analysis_options.yaml @@ -0,0 +1,10 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - avoid_late_keyword: + allow_initialized: false + ignored_types: + - Animation diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart b/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart new file mode 100644 index 00000000..04dbfe1d --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/avoid_late_keyword_allow_initialized_test.dart @@ -0,0 +1,76 @@ +// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name +// ignore_for_file: avoid_global_state + +abstract class Animation {} + +class AnimationController implements Animation {} + +class SubAnimationController extends AnimationController {} + +class ColorTween {} + +/// Check "late" keyword fail +/// +/// `avoid_late_keyword` +/// allow_initialized option disabled +class AvoidLateKeyword { + late final Animation animation1; + + late final animation2 = AnimationController(); + + late final animation3 = SubAnimationController(); + + /// expect_lint: avoid_late_keyword + late final ColorTween colorTween1; + + /// expect_lint: avoid_late_keyword + late final colorTween2 = ColorTween(); + + /// expect_lint: avoid_late_keyword + late final colorTween3 = colorTween2; + + late final AnimationController controller1; + + /// expect_lint: avoid_late_keyword + late final field1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String field2; + + /// expect_lint: avoid_late_keyword + late final String field3 = 'string'; + + /// expect_lint: avoid_late_keyword + late final field4; + + void test() { + late final Animation animation1; + + late final animation2 = AnimationController(); + + late final animation3 = SubAnimationController(); + + /// expect_lint: avoid_late_keyword + late final ColorTween colorTween1; + + /// expect_lint: avoid_late_keyword + late final colorTween2 = ColorTween(); + + /// expect_lint: avoid_late_keyword + late final colorTween3 = colorTween2; + + late final AnimationController controller1; + + /// expect_lint: avoid_late_keyword + late final local1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String local2; + + /// expect_lint: avoid_late_keyword + late final String local4 = 'string'; + + /// expect_lint: avoid_late_keyword + late final local3; + } +} diff --git a/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml b/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml new file mode 100644 index 00000000..3fd63997 --- /dev/null +++ b/lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml @@ -0,0 +1,11 @@ +name: avoid_late_keyword_allow_initialized_test +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + +dev_dependencies: + solid_lints: + path: ../../ diff --git a/lint_test/avoid_late_keyword_test.dart b/lint_test/avoid_late_keyword_test.dart new file mode 100644 index 00000000..67ce76e3 --- /dev/null +++ b/lint_test/avoid_late_keyword_test.dart @@ -0,0 +1,64 @@ +// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name +// ignore_for_file: avoid_global_state + +class ColorTween {} + +class AnimationController {} + +class SubAnimationController extends AnimationController {} + +class NotAllowed {} + +/// Check "late" keyword fail +/// +/// `avoid_late_keyword` +/// allow_initialized option enabled +class AvoidLateKeyword { + late final ColorTween colorTween; + + late final AnimationController controller1; + + late final SubAnimationController controller2; + + late final controller3 = AnimationController(); + + late final controller4 = SubAnimationController(); + + late final field1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String field2; + + /// expect_lint: avoid_late_keyword + late final field3; + + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final na2 = NotAllowed(); + + void test() { + late final ColorTween colorTween; + + late final AnimationController controller1; + + late final SubAnimationController controller2; + + late final controller3 = AnimationController(); + + late final controller4 = SubAnimationController(); + + late final local1 = 'string'; + + /// expect_lint: avoid_late_keyword + late final String local2; + + /// expect_lint: avoid_late_keyword + late final local3; + + /// expect_lint: avoid_late_keyword + late final NotAllowed na1; + + late final na2 = NotAllowed(); + } +} diff --git a/lint_test/avoid_non_null_assertion_test.dart b/lint_test/avoid_non_null_assertion_test.dart new file mode 100644 index 00000000..2a33a7df --- /dev/null +++ b/lint_test/avoid_non_null_assertion_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: avoid_global_state, prefer_match_file_name +// ignore_for_file: member_ordering + +/// Check "bang" operator fail +/// +/// `avoid_non_null_assertion` +class AvoidNonNullAssertion { + AvoidNonNullAssertion? object; + int? number; + + void test() { + // expect_lint: avoid_non_null_assertion + number!; + + // expect_lint: avoid_non_null_assertion + object!.number!; + + // expect_lint: avoid_non_null_assertion + object!.test(); + + // No lint on maps + final map = {'key': 'value'}; + map['key']!; + } +} diff --git a/lint_test/avoid_returning_widget_test.dart b/lint_test/avoid_returning_widget_test.dart new file mode 100644 index 00000000..ed366e1a --- /dev/null +++ b/lint_test/avoid_returning_widget_test.dart @@ -0,0 +1,32 @@ +// ignore_for_file: unused_element, prefer_match_file_name +// ignore_for_file: member_ordering + +/// Check returning a widget fail +/// `avoid_returning_widgets` + +import 'package:flutter/material.dart'; + +// expect_lint: avoid_returning_widgets +Widget avoidReturningWidgets() => const SizedBox(); + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + // expect_lint: avoid_returning_widgets + Widget _test1() => const SizedBox(); + + // expect_lint: avoid_returning_widgets + Widget _test2() { + return const SizedBox( + child: SizedBox(), + ); + } + + // expect_lint: avoid_returning_widgets + Widget get _test3 => const SizedBox(); + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} diff --git a/lint_test/avoid_unnecessary_setstate_test.dart b/lint_test/avoid_unnecessary_setstate_test.dart new file mode 100644 index 00000000..51b5e2be --- /dev/null +++ b/lint_test/avoid_unnecessary_setstate_test.dart @@ -0,0 +1,101 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// ignore_for_file: member_ordering, prefer_match_file_name + +import 'package:flutter/material.dart'; + +/// Check unnecessary setstate fail +/// `avoid_unnecessary_setstate` +class MyWidget extends StatefulWidget { + @override + _MyWidgetState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + void initState() { + super.initState(); + + // expect_lint: avoid_unnecessary_setstate + setState(() { + _myString = "Hello"; + }); + + if (_condition) { + // expect_lint: avoid_unnecessary_setstate + setState(() { + _myString = "Hello"; + }); + } + + // expect_lint: avoid_unnecessary_setstate + myStateUpdateMethod(); + } + + @override + void didUpdateWidget(MyWidget oldWidget) { + super.didUpdateWidget(oldWidget); + // expect_lint: avoid_unnecessary_setstate + setState(() { + _myString = "Hello"; + }); + } + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + // expect_lint: avoid_unnecessary_setstate + setState(() { + _myString = "Hello"; + }); + + if (_condition) { + // expect_lint: avoid_unnecessary_setstate + setState(() { + _myString = "Hello"; + }); + } + + // expect_lint: avoid_unnecessary_setstate + myStateUpdateMethod(); + + return ElevatedButton( + onPressed: myStateUpdateMethod, + onLongPress: () { + setState(() { + _myString = 'data'; + }); + }, + child: Text(_myString), + ); + } +} diff --git a/lint_test/avoid_unnecessary_type_assertions_test.dart b/lint_test/avoid_unnecessary_type_assertions_test.dart new file mode 100644 index 00000000..c95e47f6 --- /dev/null +++ b/lint_test/avoid_unnecessary_type_assertions_test.dart @@ -0,0 +1,36 @@ +// ignore_for_file: prefer_const_declarations +// ignore_for_file: unnecessary_nullable_for_final_variable_declarations +// ignore_for_file: unnecessary_type_check +// ignore_for_file: unused_local_variable + +/// Check the `avoid_unnecessary_type_assertions` rule + +void fun() { + final testList = [1.0, 2.0, 3.0]; + // expect_lint: avoid_unnecessary_type_assertions + final result = testList is List; + + // expect_lint: avoid_unnecessary_type_assertions + final negativeResult = testList is! List; + + // to check quick-fix => testList.length + // expect_lint: avoid_unnecessary_type_assertions + testList.whereType().length; + + final dynamicList = [1.0, 2.0]; + dynamicList.whereType(); + + // expect_lint: avoid_unnecessary_type_assertions + [1.0, 2.0].whereType(); + + final double d = 2.0; + // expect_lint: avoid_unnecessary_type_assertions + final casted = d is double; + + // expect_lint: avoid_unnecessary_type_assertions + final negativeCasted = d is! double; + + final double? nullableD = 2.0; + // casting `Type? is Type` is allowed + final castedD = nullableD is double; +} diff --git a/lint_test/avoid_unnecessary_type_casts_test.dart b/lint_test/avoid_unnecessary_type_casts_test.dart new file mode 100644 index 00000000..dffa6903 --- /dev/null +++ b/lint_test/avoid_unnecessary_type_casts_test.dart @@ -0,0 +1,35 @@ +// ignore_for_file: prefer_const_declarations +// ignore_for_file: unnecessary_nullable_for_final_variable_declarations +// ignore_for_file: unnecessary_cast +// ignore_for_file: unused_local_variable + +/// Check the `avoid_unnecessary_type_casts` rule + +void fun() { + final testList = [1.0, 2.0, 3.0]; + + // to check quick-fix => testList + // expect_lint: avoid_unnecessary_type_casts + final result = testList as List; + + final double? nullableD = 2.0; + // casting `Type? is Type` is allowed + final castedD = nullableD as double; + + final testMap = {'A': 'B'}; + + // expect_lint: avoid_unnecessary_type_casts + final castedMapValue = testMap['A'] as String?; + + // casting `Type? is Type` is allowed + final castedNotNullMapValue = testMap['A'] as String; + + final testString = 'String'; + // expect_lint: avoid_unnecessary_type_casts + _testFun(testString as String); +} + +void _testFun(String a) { + // expect_lint: avoid_unnecessary_type_casts + final result = (a as String).length; +} diff --git a/lint_test/avoid_unrelated_type_assertions_test.dart b/lint_test/avoid_unrelated_type_assertions_test.dart new file mode 100644 index 00000000..eee0a09a --- /dev/null +++ b/lint_test/avoid_unrelated_type_assertions_test.dart @@ -0,0 +1,33 @@ +// ignore_for_file: prefer_const_declarations, prefer_match_file_name +// ignore_for_file: unnecessary_nullable_for_final_variable_declarations +// ignore_for_file: unused_local_variable + +/// Check the `avoid_unrelated_type_assertions` rule +class Foo {} + +class Bar {} + +class ChildFoo extends Foo {} + +void fun() { + final testString = ''; + final testList = [1, 2, 3]; + final testMap = {'A': 'B'}; + final Foo foo = Foo(); + final childFoo = ChildFoo(); + + // expect_lint: avoid_unrelated_type_assertions + final result = testString is int; + + // expect_lint: avoid_unrelated_type_assertions + final result2 = testList is List; + + // expect_lint: avoid_unrelated_type_assertions + final result3 = foo is Bar; + + // expect_lint: avoid_unrelated_type_assertions + final result4 = childFoo is Bar; + + // expect_lint: avoid_unrelated_type_assertions + final result5 = testMap['A'] is double; +} diff --git a/lint_test/avoid_unused_parameters_test.dart b/lint_test/avoid_unused_parameters_test.dart new file mode 100644 index 00000000..4d3950f1 --- /dev/null +++ b/lint_test/avoid_unused_parameters_test.dart @@ -0,0 +1,121 @@ +// ignore_for_file: prefer_const_declarations, prefer_match_file_name +// ignore_for_file: unnecessary_nullable_for_final_variable_declarations +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_element +// ignore_for_file: newline_before_return +// ignore_for_file: no_empty_block +// ignore_for_file: member_ordering + +import 'package:flutter/material.dart'; + +/// Check the `avoid_unused_parameters` rule + +// expect_lint: avoid_unused_parameters +void fun(String s) { + return; +} + +class TestClass { + // expect_lint: avoid_unused_parameters + static void staticMethod(int a) {} + + // expect_lint: avoid_unused_parameters + void method(String s) { + return; + } + + void methodWithUnderscores(int _) {} +} + +class TestClass2 { + void method(String _) { + return; + } +} + +class SomeOtherClass { + // expect_lint: avoid_unused_parameters + void method(String s) { + return; + } +} + +class SomeAnotherClass extends SomeOtherClass { + @override + void method(String s) {} +} + +void someOtherFunction(String s) { + print(s); + return; +} + +class SomeOtherAnotherClass { + void method(String s) { + print(s); + return; + } + + // expect_lint: avoid_unused_parameters + void anonymousCallback(Function(int a) cb) {} +} + +// expect_lint: avoid_unused_parameters +void closure(int a) { + void internal(int a) { + print(a); + } +} + +typedef MaxFun = int Function(int a, int b); + +// Allowed same way as override +final MaxFun maxFunInstance = (int a, int b) => 1; + +class Foo { + final int a; + final int? b; + + Foo._(this.a, this.b); + + Foo.name(this.a, this.b); + + Foo.coolName({required this.a, required this.b}); + + // expect_lint: avoid_unused_parameters + Foo.another({required int c}) + : a = 1, + b = 0; + + // expect_lint: avoid_unused_parameters + factory Foo.aOnly(int a) { + return Foo._(1, null); + } +} + +class Bar extends Foo { + Bar.name(super.a, super.b) : super.name(); +} + +class TestWidget extends StatelessWidget { + const TestWidget({ + super.key, + // expect_lint: avoid_unused_parameters + int a = 1, + }); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} + +class UsingConstructorParameterInInitializer { + final int _value; + + UsingConstructorParameterInInitializer(int value) : _value = value; + + void printValue() { + print(_value); + } +} diff --git a/lint_test/cyclomatic_complexity_test.dart b/lint_test/cyclomatic_complexity_test.dart new file mode 100644 index 00000000..52cf2bbf --- /dev/null +++ b/lint_test/cyclomatic_complexity_test.dart @@ -0,0 +1,16 @@ +// ignore_for_file: literal_only_boolean_expressions +// ignore_for_file: no_empty_block + +/// Check complexity fail +/// +/// `cyclomatic_complexity_metric: max_complexity` +/// expect_lint: cyclomatic_complexity +void cyclomaticComplexity() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +} diff --git a/lint_test/double_literal_format_test.dart b/lint_test/double_literal_format_test.dart new file mode 100644 index 00000000..195b01e8 --- /dev/null +++ b/lint_test/double_literal_format_test.dart @@ -0,0 +1,38 @@ +// ignore_for_file: avoid_global_state +// ignore_for_file: type_annotate_public_apis +// ignore_for_file: unused_local_variable + +/// Check the `double_literal_format` rule + +// expect_lint: double_literal_format +var badA = 05.23; +var goodA = 5.23; +var stringA = '05.23'; + +var intA = 0; + +// expect_lint: double_literal_format +double badB = -01.2; +double goodB = -1.2; + +// expect_lint: double_literal_format +double badC = -001.2; + +// expect_lint: double_literal_format +double badExpr = 5.23 + 05.23; + +double goodExpr = 5.23 + 5.23; + +class DoubleLiteralFormatTest { + // expect_lint: double_literal_format + var badA = .16e+5; + var goodA = 0.16e+5; + + void someMethod() { + // expect_lint: double_literal_format + const badA = -0.250; + const goodA = -0.25; + // expect_lint: double_literal_format + const badB = 0.160e+5; + } +} diff --git a/lint_test/lines_of_code_test.dart b/lint_test/lines_of_code_test.dart new file mode 100644 index 00000000..200cbee9 --- /dev/null +++ b/lint_test/lines_of_code_test.dart @@ -0,0 +1,61 @@ +// ignore_for_file: no_magic_number + +import 'package:test/test.dart'; + +/// Check number of lines fail +/// +/// `function_lines_of_code: max_lines` +/// expect_lint: function_lines_of_code +void linesOfCode() { + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); + test("addition", () { + expect(1 + 1, equals(2)); + }); +} diff --git a/lint_test/member_ordering_test.dart b/lint_test/member_ordering_test.dart new file mode 100644 index 00000000..3faf21f0 --- /dev/null +++ b/lint_test/member_ordering_test.dart @@ -0,0 +1,232 @@ +// ignore_for_file: unused_field, prefer_match_file_name, proper_super_calls +// ignore_for_file: unused_element +// ignore_for_file: no_empty_block + +import 'package:flutter/widgets.dart'; + +/// Check the `member_ordering` rule + +class AlphabeticalClass { + final b = 1; + + // expect_lint: member_ordering + final a = 1; + final c = 1; + + void bStuff() {} + + // expect_lint: member_ordering + void aStuff() {} + + void cStuff() {} + + void visitStatement() {} + + // expect_lint: member_ordering + void visitStanford() {} +} + +class CorrectOrder { + final publicField = 1; + int _privateField = 2; + + CorrectOrder(); + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void publicDoStuff() {} + + void _privateDoStuff() {} + + void close() {} +} + +class WrongOrder { + void close() {} + + // expect_lint: member_ordering + void _privateDoStuff() {} + + // expect_lint: member_ordering + void publicDoStuff() {} + + // expect_lint: member_ordering + void set privateFieldSetter(int value) { + _privateField = value; + } + + // expect_lint: member_ordering + int get privateFieldGetter => _privateField; + + // expect_lint: member_ordering + WrongOrder(); + + // expect_lint: member_ordering + int _privateField = 2; + + // expect_lint: member_ordering + final publicField = 1; +} + +class PartiallyWrongOrder { + final publicField = 1; + + PartiallyWrongOrder(); + + // expect_lint: member_ordering + int _privateField = 2; + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void _privateDoStuff() {} + + // expect_lint: member_ordering + void publicDoStuff() {} + + void close() {} +} + +class CorrectWidget extends StatefulWidget { + @override + State createState() => _CorrectWidgetState(); + + const CorrectWidget({super.key}); +} + +class _CorrectWidgetState extends State { + static const constField = 1; + static final staticField = 1; + + static void staticDoStuff() {} + + final publicField = 1; + final _privateField = 1; + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _CorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + @override + void initState() => super.initState(); + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + @override + void didUpdateWidget(covariant CorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} + +class WrongWidget extends StatefulWidget { + const WrongWidget({super.key}); + + // expect_lint: member_ordering + @override + State createState() => _WrongWidgetState(); +} + +class _WrongWidgetState extends State { + @override + void dispose() => super.dispose(); + + // expect_lint: member_ordering + @override + void didUpdateWidget(covariant WrongWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + // expect_lint: member_ordering + @override + void didChangeDependencies() => super.didChangeDependencies(); + + // expect_lint: member_ordering + @override + void initState() => super.initState(); + + // expect_lint: member_ordering + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + // expect_lint: member_ordering + _WrongWidgetState(); + + // expect_lint: member_ordering + void _privateDoStuff() {} + + // expect_lint: member_ordering + void publicDoStuff() {} + + // expect_lint: member_ordering + final _privateField = 1; + + // expect_lint: member_ordering + final publicField = 1; + + // expect_lint: member_ordering + static void staticDoStuff() {} + + // expect_lint: member_ordering + static final staticField = 1; + + // expect_lint: member_ordering + static const constField = 1; +} + +class PartiallyCorrectWidget extends StatefulWidget { + @override + State createState() => _PartiallyCorrectWidgetState(); + + const PartiallyCorrectWidget({super.key}); +} + +class _PartiallyCorrectWidgetState extends State { + static final staticField = 1; + + // expect_lint: member_ordering + static const constField = 1; + + static void staticDoStuff() {} + + final _privateField = 1; + + // expect_lint: member_ordering + final publicField = 1; + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _PartiallyCorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + // expect_lint: member_ordering + @override + void initState() => super.initState(); + + @override + void didUpdateWidget(covariant PartiallyCorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} diff --git a/lint_test/newline_before_return_test.dart b/lint_test/newline_before_return_test.dart new file mode 100644 index 00000000..282057ad --- /dev/null +++ b/lint_test/newline_before_return_test.dart @@ -0,0 +1,37 @@ +// ignore_for_file: unused_local_variable, prefer_match_file_name +// ignore_for_file: member_ordering +// ignore_for_file: avoid_unused_parameters + +/// Check the `newline_before_return` rule +class Foo { + int method() { + final a = 0; + // expect_lint: newline_before_return + return 1; + } + + void anotherMethod() { + final a = 1; + // expect_lint: newline_before_return + return; + } + + void bar(void Function()) { + return; + } +} + +void fun() { + final foo = Foo(); + foo.bar(() { + // This comment is ignored and line above is checked to be a newline + return; + }); + foo.bar(() { + final a = 1; + // expect_lint: newline_before_return + return; + }); + // expect_lint: newline_before_return + return; +} diff --git a/lint_test/no_empty_block_test.dart b/lint_test/no_empty_block_test.dart new file mode 100644 index 00000000..036e406f --- /dev/null +++ b/lint_test/no_empty_block_test.dart @@ -0,0 +1,51 @@ +// ignore_for_file: prefer_const_declarations, prefer_match_file_name +// ignore_for_file: unused_local_variable +// ignore_for_file: cyclomatic_complexity +// ignore_for_file: avoid_unused_parameters + +/// Check the `no_empty_block` rule + +// expect_lint: no_empty_block +void fun() {} + +void anotherFun() { + if (true) { + if (true) { + // expect_lint: no_empty_block + if (true) {} + } + } +} + +// to-do comments are allowed +void toDoStuff() { + // TODO: Implement doStuff function +} + +void catchStuff() { + // expect_lint: no_empty_block + try {} catch (e) {} + + try { + print('do stuff'); + // empty catch block is allowed + } catch (e) {} +} + +void nestedFun(void Function() fun) { + return; +} + +void doStuff() { + // expect_lint: no_empty_block + nestedFun(() {}); +} + +class A { + // expect_lint: no_empty_block + void method() {} + + void toDoMethod() { + // TODO: implement toDoMethod + } +} diff --git a/lint_test/no_equal_then_else_test.dart b/lint_test/no_equal_then_else_test.dart new file mode 100644 index 00000000..c9caa40c --- /dev/null +++ b/lint_test/no_equal_then_else_test.dart @@ -0,0 +1,30 @@ +// ignore_for_file: unused_local_variable +// ignore_for_file: cyclomatic_complexity +// ignore_for_file: no_magic_number +// ignore_for_file: prefer_conditional_expressions + +/// Check the `no_equal_then_else` rule +void fun() { + final _valueA = 1; + final _valueB = 2; + + int _result = 0; + + // expect_lint: no_equal_then_else + if (_valueA == 1) { + _result = _valueA; + } else { + _result = _valueA; + } + + if (_valueA == 1) { + _result = _valueA; + } else { + _result = _valueB; + } + + // expect_lint: no_equal_then_else + _result = _valueA == 2 ? _valueA : _valueA; + + _result = _valueA == 2 ? _valueA : _valueB; +} diff --git a/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml b/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml new file mode 100644 index 00000000..8c1644ac --- /dev/null +++ b/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml @@ -0,0 +1,8 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - no_magic_number: + allowed_in_widget_params: true diff --git a/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart b/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart new file mode 100644 index 00000000..a01e12fa --- /dev/null +++ b/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart @@ -0,0 +1,27 @@ +// Allowed for numbers in a Widget subtype parameters. +abstract interface class Widget {} + +class StatelessWidget implements Widget {} + +class MyWidget extends StatelessWidget { + final MyWidgetDecoration decoration; + final int value; + + MyWidget({ + required this.decoration, + required this.value, + }); +} + +class MyWidgetDecoration { + final int size; + + MyWidgetDecoration({required this.size}); +} + +Widget build() { + return MyWidget( + decoration: MyWidgetDecoration(size: 12), + value: 23, + ); +} diff --git a/lint_test/no_magic_number_allowed_in_widget_params_test/pubspec.yaml b/lint_test/no_magic_number_allowed_in_widget_params_test/pubspec.yaml new file mode 100644 index 00000000..b5d3efe4 --- /dev/null +++ b/lint_test/no_magic_number_allowed_in_widget_params_test/pubspec.yaml @@ -0,0 +1,11 @@ +name: no_magic_number_allowed_in_widget_params_test +description: A starting point for Dart libraries or applications. +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dev_dependencies: + solid_lints: + path: ../../ + test: ^1.20.1 diff --git a/lint_test/no_magic_number_test.dart b/lint_test/no_magic_number_test.dart new file mode 100644 index 00000000..3f94784e --- /dev/null +++ b/lint_test/no_magic_number_test.dart @@ -0,0 +1,117 @@ +// ignore_for_file: unused_local_variable +// ignore_for_file: prefer_match_file_name +// ignore_for_file: avoid_unused_parameters +// ignore_for_file: no_empty_block + +/// Check the `no_magic_number` rule + +const pi = 3.14; +const radiusToDiameterCoefficient = 2; + +// expect_lint: no_magic_number +double circumference(double radius) => 2 * 3.14 * radius; + +double correctCircumference(double radius) => + radiusToDiameterCoefficient * pi * radius; + +bool canDrive(int age, {bool isUSA = false}) { +// expect_lint: no_magic_number + return isUSA ? age >= 16 : age > 18; +} + +const usaDrivingAge = 16; +const worldWideDrivingAge = 18; + +bool correctCanDrive(int age, {bool isUSA = false}) { + return isUSA ? age >= usaDrivingAge : age > worldWideDrivingAge; +} + +class ConstClass { + final int a; + + const ConstClass(this.a); +} + +enum ConstEnum { + // Allowed in enum arguments + one(1), + two(2); + + final int value; + + const ConstEnum(this.value); +} + +void fun() { + // Allowed in const constructors + const classInstance = ConstClass(1); + + // Allowed in list literals + final list = [1, 2, 3]; + + // Allowed int map literals + final map = {1: 'One', 2: 'Two'}; + + // Allowed in indexed expression + final result = list[1]; + + // Allowed in DateTime because it doesn't have const constructor + final apocalypse = DateTime(2012, 12, 21); +} + +// Allowed for defaults in constructors and methods. +class DefaultValues { + final int value; + + DefaultValues.named({ + this.value = 2, + }); + + DefaultValues.positional([ + this.value = 3, + ]); + + void methodWithNamedParam({int value = 4}) {} + + void methodWithPositionalParam([int value = 5]) {} +} + +void topLevelFunctionWithDefaultParam({int value = 6}) { + ({int value = 7}) {}; +} + +// Allowed for numbers in constructor initializer. +class ConstructorInitializer { + final int value; + + ConstructorInitializer() : value = 10; +} + +abstract interface class Widget {} + +class StatelessWidget implements Widget {} + +class MyWidget extends StatelessWidget { + final MyWidgetDecoration decoration; + final int value; + + MyWidget({ + required this.decoration, + required this.value, + }); +} + +class MyWidgetDecoration { + final int size; + + MyWidgetDecoration({required this.size}); +} + +Widget build() { + return MyWidget( + // expect_lint: no_magic_number + decoration: MyWidgetDecoration(size: 12), + // expect_lint: no_magic_number + value: 23, + ); +} diff --git a/lint_test/number_of_parameters_test.dart b/lint_test/number_of_parameters_test.dart new file mode 100644 index 00000000..12fa6e21 --- /dev/null +++ b/lint_test/number_of_parameters_test.dart @@ -0,0 +1,7 @@ +/// Check number of parameters fail +/// +/// `number_of_parameters: max_parameters` +/// expect_lint: number_of_parameters +String numberOfParameters(String a, String b, String c) { + return a + b + c; +} diff --git a/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml b/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml new file mode 100644 index 00000000..750be4b4 --- /dev/null +++ b/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml @@ -0,0 +1,8 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - prefer_conditional_expressions: + ignore_nested: true diff --git a/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart b/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart new file mode 100644 index 00000000..954e77fa --- /dev/null +++ b/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart @@ -0,0 +1,15 @@ +// ignore_for_file: unused_local_variable +// ignore_for_file: cyclomatic_complexity +// ignore_for_file: no_equal_then_else + +/// Check the `prefer_conditional_expressions` rule ignore_nested option +void fun() { + int _result = 0; + + // Allowed because ignore_nested flag is enabled + if (1 > 0) { + _result = 1 > 2 ? 2 : 1; + } else { + _result = 0; + } +} diff --git a/lint_test/prefer_conditional_expressions_ignore_nested_test/pubspec.yaml b/lint_test/prefer_conditional_expressions_ignore_nested_test/pubspec.yaml new file mode 100644 index 00000000..188938a9 --- /dev/null +++ b/lint_test/prefer_conditional_expressions_ignore_nested_test/pubspec.yaml @@ -0,0 +1,15 @@ +name: prefer_conditional_expressions_ignore_nested_test +description: A starting point for Dart libraries or applications. +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + solid_lints: + path: ../../ + test: ^1.20.1 diff --git a/lint_test/prefer_conditional_expressions_test.dart b/lint_test/prefer_conditional_expressions_test.dart new file mode 100644 index 00000000..ee60f237 --- /dev/null +++ b/lint_test/prefer_conditional_expressions_test.dart @@ -0,0 +1,47 @@ +// ignore_for_file: unused_local_variable +// ignore_for_file: cyclomatic_complexity +// ignore_for_file: no_equal_then_else +// ignore_for_file: dead_code +// ignore_for_file: no_magic_number + +/// Check the `prefer_conditional_expressions` rule +void fun() { + int _result = 0; + + // expect_lint: prefer_conditional_expressions + if (true) { + _result = 1; + } else { + _result = 2; + } + + // expect_lint: prefer_conditional_expressions + if (1 > 0) + _result = 1; + else + _result = 2; + + // expect_lint: prefer_conditional_expressions + if (1 > 0) { + _result = 1 > 2 ? 2 : 1; + } else { + _result = 0; + } +} + +int someFun() { + // expect_lint: prefer_conditional_expressions + if (1 == 1) { + return 0; + } else { + return 1; + } +} + +int anotherFun() { + // expect_lint: prefer_conditional_expressions + if (1 > 0) + return 1; + else + return 2; +} diff --git a/lint_test/prefer_first_test.dart b/lint_test/prefer_first_test.dart new file mode 100644 index 00000000..2527d828 --- /dev/null +++ b/lint_test/prefer_first_test.dart @@ -0,0 +1,21 @@ +/// Check the `prefer_first` rule +void fun() { + const zero = 0; + final list = [0, 1, 2, 3]; + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + // expect_lint: prefer_first + list[0]; + list[zero]; + // expect_lint: prefer_first + list.elementAt(0); + list.elementAt(zero); + // expect_lint: prefer_first + set.elementAt(0); + + // expect_lint: prefer_first + map.keys.elementAt(0); + // expect_lint: prefer_first + map.values.elementAt(0); +} diff --git a/lint_test/prefer_last_test.dart b/lint_test/prefer_last_test.dart new file mode 100644 index 00000000..4b87476f --- /dev/null +++ b/lint_test/prefer_last_test.dart @@ -0,0 +1,25 @@ +/// Check the `prefer_first` rule +void fun() { + final list = [0, 1, 2, 3]; + final length = list.length - 1; + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + // expect_lint: prefer_last + list[list.length - 1]; + + list[length - 1]; + + // expect_lint: prefer_last + list.elementAt(list.length - 1); + list.elementAt(length - 1); + + // expect_lint: prefer_last + set.elementAt(set.length - 1); + + // expect_lint: prefer_last + map.keys.elementAt(map.keys.length - 1); + + // expect_lint: prefer_last + map.values.elementAt(map.values.length - 1); +} diff --git a/lint_test/prefer_match_file_name_enum_test.dart b/lint_test/prefer_match_file_name_enum_test.dart new file mode 100644 index 00000000..91da9904 --- /dev/null +++ b/lint_test/prefer_match_file_name_enum_test.dart @@ -0,0 +1,5 @@ +// ignore_for_file: unused_element, unused_field + +/// Check the `prefer_match_file_name` rule +// expect_lint: prefer_match_file_name +enum WrongMixin { a, b } diff --git a/lint_test/prefer_match_file_name_extension_test.dart b/lint_test/prefer_match_file_name_extension_test.dart new file mode 100644 index 00000000..dd5ea1fc --- /dev/null +++ b/lint_test/prefer_match_file_name_extension_test.dart @@ -0,0 +1,5 @@ +// ignore_for_file: unused_element, unused_field + +/// Check the `prefer_match_file_name` rule +// expect_lint: prefer_match_file_name +extension WrongExtension on List {} diff --git a/lint_test/prefer_match_file_name_mixin_test.dart b/lint_test/prefer_match_file_name_mixin_test.dart new file mode 100644 index 00000000..5b8d2331 --- /dev/null +++ b/lint_test/prefer_match_file_name_mixin_test.dart @@ -0,0 +1,5 @@ +// ignore_for_file: unused_element, unused_field + +/// Check the `prefer_match_file_name` rule +// expect_lint: prefer_match_file_name +mixin WrongMixin {} diff --git a/lint_test/prefer_match_file_name_test.dart b/lint_test/prefer_match_file_name_test.dart new file mode 100644 index 00000000..68466050 --- /dev/null +++ b/lint_test/prefer_match_file_name_test.dart @@ -0,0 +1,17 @@ +// ignore_for_file: unused_element, unused_field + +/// Check the `prefer_match_file_name` rule +class _AnotherPrivateClass {} + +class PreferMatchFileNameTest {} + +/// Only first public element declaration is checked +class WrongClass {} + +class _PrivateClass {} + +extension _PrivateExtension on PreferMatchFileNameTest {} + +enum _PrivateEnum { a, b } + +mixin _PrivateMixin {} diff --git a/lint_test/prefer_match_file_name_wrong_test.dart b/lint_test/prefer_match_file_name_wrong_test.dart new file mode 100644 index 00000000..b0588354 --- /dev/null +++ b/lint_test/prefer_match_file_name_wrong_test.dart @@ -0,0 +1,10 @@ +// ignore_for_file: unused_element, unused_field + +/// Check the `prefer_match_file_name` rule +class _AnotherPrivateClass {} + +// expect_lint: prefer_match_file_name +class WrongClass {} + +/// Only first public element declaration is checked +class PreferMatchFileNameWrongTest {} diff --git a/lint_test/proper_super_calls_test.dart b/lint_test/proper_super_calls_test.dart new file mode 100644 index 00000000..2bc794b9 --- /dev/null +++ b/lint_test/proper_super_calls_test.dart @@ -0,0 +1,77 @@ +// ignore_for_file: prefer_match_file_name, no_empty_block + +class Widget {} + +abstract class State { + build(); + dispose() {} + initState() {} +} + +abstract class StatefulWidget extends Widget { + State createState(); + + StatefulWidget(); +} + +/// Check "check super" keyword fail +/// +/// `proper_super_calls` +class ProperSuperCallsTest1 extends StatefulWidget { + @override + State createState() => _ProperSuperCallsTest1State(); + + ProperSuperCallsTest1(); +} + +class _ProperSuperCallsTest1State extends State { + @override + Widget build() { + return Widget(); + } + + // expect_lint: proper_super_calls + @override + void initState() { + print(''); + super.initState(); + } + + // expect_lint: proper_super_calls + @override + void dispose() { + super.dispose(); + print(''); + } +} + +class ProperSuperCallsTest2 extends StatefulWidget { + @override + State createState() => _ProperSuperCallsTest2State(); + + ProperSuperCallsTest2(); +} + +class _ProperSuperCallsTest2State extends State { + @override + Widget build() { + return Widget(); + } + + @override + void initState() { + super.initState(); + print(''); + } + + @override + void dispose() { + print(''); + super.dispose(); + } +} + +class MyClass { + dispose() {} + initState() {} +} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml new file mode 100644 index 00000000..cb37cd0b --- /dev/null +++ b/lint_test/pubspec.yaml @@ -0,0 +1,15 @@ +name: solid_lints_test +description: A starting point for Dart libraries or applications. +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + solid_lints: + path: ../ + test: ^1.20.1 diff --git a/pubspec.yaml b/pubspec.yaml index c348fe06..d2f149d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,12 +2,21 @@ name: solid_lints description: Lints for Dart and Flutter based on software industry standards and best practices. -version: 0.0.19 +version: 0.1.0 homepage: https://github.com/solid-software/solid_lints/ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - # 5.7.6 has proprietary license so we will not use it - dart_code_metrics: '>=5.7.3 <=5.7.5' + analyzer: ^5.12.0 + collection: ^1.17.2 + custom_lint_builder: ^0.5.6 + path: ^1.8.3 + +dev_dependencies: + # These packages are mandatory for some of tests + flutter: + sdk: flutter + test: ^1.24.6 +