From a287f67f3fb46fcfe198d082f41e03626ad8555b Mon Sep 17 00:00:00 2001 From: Ryunosuke Muramatsu Date: Wed, 3 Dec 2025 16:12:11 +0900 Subject: [PATCH 1/3] feat: Migrate to analysis_server_plugin from custom_lint --- .github/workflows/code-check.yml | 18 +- packages/altive_lints/README.md | 149 ++--- packages/altive_lints/analysis_options.yaml | 9 +- .../example/analysis_options.yaml | 4 - packages/altive_lints/example/assists.dart | 47 ++ ...oid_consecutive_sliver_to_box_adapter.dart | 71 --- .../example/lints/avoid_hardcoded_color.dart | 25 - .../lints/avoid_hardcoded_color_test.dart | 28 - .../lints/avoid_hardcoded_japanese.dart | 12 - .../lints/avoid_hardcoded_japanese_test.dart | 10 - .../lints/avoid_shrink_wrap_in_list_view.dart | 27 - .../example/lints/avoid_single_child.dart | 201 ------- .../example/lints/prefer_clock_now.dart | 6 - .../prefer_dedicated_media_query_methods.dart | 19 - .../lints/prefer_space_between_elements.dart | 21 - .../prefer_to_include_sliver_in_name.dart | 65 --- packages/altive_lints/example/pubspec.yaml | 3 +- packages/altive_lints/example/rules.dart | 122 ++++ .../example/test/avoid_hardcoded_color.dart | 27 - .../test/avoid_hardcoded_japanese.dart | 9 - packages/altive_lints/lib/altive_lints.dart | 39 -- packages/altive_lints/lib/altive_lints.yaml | 4 + packages/altive_lints/lib/main.dart | 43 ++ .../assists/add_macro_document_comments.dart | 107 ++-- .../add_macro_template_document_comment.dart | 92 +-- ..._with_macro_template_document_comment.dart | 105 ++-- ...oid_consecutive_sliver_to_box_adapter.dart | 67 ++- .../lib/src/lints/avoid_hardcoded_color.dart | 181 ++++-- .../src/lints/avoid_hardcoded_japanese.dart | 79 ++- .../lints/avoid_shrink_wrap_in_list_view.dart | 51 +- .../lib/src/lints/avoid_single_child.dart | 126 ++-- .../lib/src/lints/prefer_clock_now.dart | 60 +- .../prefer_dedicated_media_query_methods.dart | 77 ++- .../lints/prefer_space_between_elements.dart | 112 ++-- .../prefer_to_include_sliver_in_name.dart | 129 +++-- .../lib/src/utils/files_utils.dart | 28 +- .../lib/src/utils/package_utils.dart | 39 ++ .../lib/src/utils/types_utils.dart | 2 +- packages/altive_lints/pubspec.yaml | 13 +- ...onsecutive_sliver_to_box_adapter_test.dart | 100 ++++ .../lints/avoid_hardcoded_japanese_test.dart | 91 +++ .../avoid_shrink_wrap_in_list_view_test.dart | 121 ++++ .../src/lints/avoid_single_child_test.dart | 543 ++++++++++++++++++ .../test/src/lints/prefer_clock_now_test.dart | 41 ++ ...er_dedicated_media_query_methods_test.dart | 122 ++++ .../prefer_space_between_elements_test.dart | 90 +++ ...prefer_to_include_sliver_in_name_test.dart | 93 +++ .../test/src/utils/files_utils_test.dart | 50 +- packages/diffscrape/pubspec.yaml | 2 +- pubspec.yaml | 26 +- 50 files changed, 2381 insertions(+), 1125 deletions(-) create mode 100644 packages/altive_lints/example/assists.dart delete mode 100644 packages/altive_lints/example/lints/avoid_consecutive_sliver_to_box_adapter.dart delete mode 100644 packages/altive_lints/example/lints/avoid_hardcoded_color.dart delete mode 100644 packages/altive_lints/example/lints/avoid_hardcoded_color_test.dart delete mode 100644 packages/altive_lints/example/lints/avoid_hardcoded_japanese.dart delete mode 100644 packages/altive_lints/example/lints/avoid_hardcoded_japanese_test.dart delete mode 100644 packages/altive_lints/example/lints/avoid_shrink_wrap_in_list_view.dart delete mode 100644 packages/altive_lints/example/lints/avoid_single_child.dart delete mode 100644 packages/altive_lints/example/lints/prefer_clock_now.dart delete mode 100644 packages/altive_lints/example/lints/prefer_dedicated_media_query_methods.dart delete mode 100644 packages/altive_lints/example/lints/prefer_space_between_elements.dart delete mode 100644 packages/altive_lints/example/lints/prefer_to_include_sliver_in_name.dart create mode 100644 packages/altive_lints/example/rules.dart delete mode 100644 packages/altive_lints/example/test/avoid_hardcoded_color.dart delete mode 100644 packages/altive_lints/example/test/avoid_hardcoded_japanese.dart delete mode 100644 packages/altive_lints/lib/altive_lints.dart create mode 100644 packages/altive_lints/lib/main.dart create mode 100644 packages/altive_lints/lib/src/utils/package_utils.dart create mode 100644 packages/altive_lints/test/src/lints/avoid_consecutive_sliver_to_box_adapter_test.dart create mode 100644 packages/altive_lints/test/src/lints/avoid_hardcoded_japanese_test.dart create mode 100644 packages/altive_lints/test/src/lints/avoid_shrink_wrap_in_list_view_test.dart create mode 100644 packages/altive_lints/test/src/lints/avoid_single_child_test.dart create mode 100644 packages/altive_lints/test/src/lints/prefer_clock_now_test.dart create mode 100644 packages/altive_lints/test/src/lints/prefer_dedicated_media_query_methods_test.dart create mode 100644 packages/altive_lints/test/src/lints/prefer_space_between_elements_test.dart create mode 100644 packages/altive_lints/test/src/lints/prefer_to_include_sliver_in_name_test.dart diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index f68a22a..8cfcdc4 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -21,18 +21,14 @@ jobs: - uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" cache: true - - name: Install Melos - uses: bluefireteam/melos-action@v3 + - name: Format + run: dart format --set-exit-if-changed packages/altive_lints/lib - - name: Analyze packages - run: melos run analyze + - name: Analyze + run: flutter analyze - - name: Custom lint - run: melos custom_lint - - - name: Check for the existence of unformatted files - # Cannot use `melos format` as it requires excluding files generated from the target file - run: melos run format:ci --no-select + - name: Test + run: dart test packages/altive_lints --fail-fast diff --git a/packages/altive_lints/README.md b/packages/altive_lints/README.md index 2244de0..346511e 100644 --- a/packages/altive_lints/README.md +++ b/packages/altive_lints/README.md @@ -1,17 +1,17 @@ # Altive Lints -Provides `all_lint_rules.yaml` that activates all rules and `altive_lints.yaml` with Altive recommended rule selection. +Provides `all_lint_rules.yaml` that activates all lint rules and `altive_lints.yaml` with Altive recommended lint rule selection. -There are also Altive-made rules by custom_lint. +`altive_lints` also includes Altive-made analysis rules created with the Analyzer Plugin (analysis_server_plugin). ## Table of content - [Table of content](#table-of-content) - [Getting started](#getting-started) - [altive\_lints](#altive_lints) - - [Enable custom\_lint](#enable-custom_lint) - - [Disabling lint rules](#disabling-lint-rules) -- [All custom-lint rules in altive\_lints](#all-custom-lint-rules-in-altive_lints) + - [Disabling lint rules/analysis rules](#disabling-lint-rulesanalysis-rules) + - [Ignoring analysis rules](#ignoring-analysis-rules) +- [All custom analysis rules in altive\_lints](#all-custom-analysis-rules-in-altive_lints) - [avoid\_consecutive\_sliver\_to\_box\_adapter](#avoid_consecutive_sliver_to_box_adapter) - [avoid\_hardcoded\_color](#avoid_hardcoded_color) - [avoid\_hardcoded\_japanese](#avoid_hardcoded_japanese) @@ -19,8 +19,8 @@ There are also Altive-made rules by custom_lint. - [avoid\_single\_child](#avoid_single_child) - [prefer\_clock\_now](#prefer_clock_now) - [prefer\_dedicated\_media\_query\_methods](#prefer_dedicated_media_query_methods) - - [prefer\_to\_include\_sliver\_in\_name](#prefer_to_include_sliver_in_name) - [prefer\_space\_between\_elements](#prefer_space_between_elements) + - [prefer\_to\_include\_sliver\_in\_name](#prefer_to_include_sliver_in_name) - [All assists in altive\_lints](#all-assists-in-altive_lints) - [Add macro template documentation comment](#add-macro-template-documentation-comment) - [Add macro documentation comment](#add-macro-documentation-comment) @@ -46,48 +46,49 @@ If not, create a new one or copy [analysis_options.yaml](https://github.com/alti include: package:altive_lints/altive_lints.yaml ``` -### Enable custom_lint - -altive_lints comes bundled with its own rules using custom_lints. - -- Add both altive_lints and custom_lint to your `pubspec.yaml`: - ```yaml - dev_dependencies: - altive_lints: - custom_lint: # <- add this - ``` -- Enable `custom_lint`'s plugin in your `analysis_options.yaml`: - - ```yaml - include: package:altive_lints/altive_lints.yaml - analyzer: - plugins: - - custom_lint - ``` - -### Disabling lint rules +### Disabling lint rules/analysis rules By default when installing altive_lints, most of the lints will be enabled. To change this, you have a few options. ```yaml include: package:altive_lints/altive_lints.yaml -analyzer: - plugins: - - custom_lint - linter: rules: # Explicitly disable one lint rule. - public_member_api_docs: false -custom_lint: - rules: - # Explicitly disable one custom-lint rule. - - avoid_hardcoded_color: false +plugins: + altive_lints: ^2.0.0 + diagnostics: + # Explicitly disable one analysis rule. + avoid_consecutive_sliver_to_box_adapter: false + avoid_hardcoded_color: false + avoid_hardcoded_japanese: false + avoid_shrink_wrap_in_list_view: false + avoid_single_child: false + prefer_clock_now: false + prefer_dedicated_media_query_methods: false + prefer_space_between_elements: false + prefer_to_include_sliver_in_name: false ``` -## All custom-lint rules in altive_lints +### Ignoring analysis rules + +You can ignore analysis rules by using `ignore: altive_lints/{rule_name}`. + +```dart +// for file. +// ignore_for_file: altive_lints/avoid_hardcoded_color + +... + +// for line. +// ignore: altive_lints/avoid_hardcoded_color +Color(0xFF00FF00); +``` + +## All custom analysis rules in altive_lints ### avoid_consecutive_sliver_to_box_adapter @@ -247,77 +248,83 @@ This is to reduce unnecessary widget rebuilding and improve performance by using ```dart var size = MediaQuery.of(context).size; // LINT +var width = MediaQuery.sizeOf(context).width; // LINT +var height = MediaQuery.sizeOf(context).height; // LINT var padding = MediaQuery.maybeOf(context)?.padding; // LINT +var viewInsets = MediaQuery.viewInsetsOf(context); // LINT ``` **Good**: ```dart var size = MediaQuery.sizeOf(context); -var padding = MediaQuery.viewInsetsOf(context); +var width = MediaQuery.widthOf(context); +var height = MediaQuery.heightOf(context); +var padding = MediaQuery.paddingOf(context); +var viewInsets = MediaQuery.viewInsetsOf(context); ``` -### prefer_to_include_sliver_in_name +### prefer_space_between_elements -Prefer to include ‘Sliver’ in the class name or named constructor of a widget that returns a Sliver-type widget. +Prefer to insert blank lines for spacing rules in class definitions. +between constructors and fields, and between constructors and build methods. + +The purpose of proper spacing is to improve code readability and organization and to make it easier to visually distinguish between different sections of a class. -This makes it easy for the user to know at a glance that it is a Sliver type Widget, and improves readability and consistency. **Bad**: ```dart -class MyCustomList extends StatelessWidget { // LINT - @override +class MyWidget extends StatelessWidget { + MyWidget(this.title); + final String title; // LINT + @override // LINT Widget build(BuildContext context) { - return SliverList(...); + return Text(title); } } - ``` **Good**: ```dart -class SliverMyCustomList extends StatelessWidget { +class MyWidget extends StatelessWidget { + MyWidget(this.title); + + final String title; + @override Widget build(BuildContext context) { - return SliverList(...); + return Text(title); } } ``` -### prefer_space_between_elements - -Prefer to insert blank lines for spacing rules in class definitions. -between constructors and fields, and between constructors and build methods. +### prefer_to_include_sliver_in_name -The purpose of proper spacing is to improve code readability and organization and to make it easier to visually distinguish between different sections of a class. +Prefer to include ‘Sliver’ in the class name or named constructor of a widget that returns a Sliver-type widget. +This makes it easy for the user to know at a glance that it is a Sliver type Widget, and improves readability and consistency. **Bad**: ```dart -class MyWidget extends StatelessWidget { - MyWidget(this.title); - final String title; // LINT - @override // LINT +class MyCustomList extends StatelessWidget { // LINT + @override Widget build(BuildContext context) { - return Text(title); + return SliverList(...); } } + ``` **Good**: ```dart -class MyWidget extends StatelessWidget { - MyWidget(this.title); - - final String title; - +class SliverMyCustomList extends StatelessWidget { @override Widget build(BuildContext context) { - return Text(title); + return SliverList(...); } } ``` @@ -356,18 +363,14 @@ When you place the cursor on a constructor or method declaration and execute **" **Before:** ```dart -void myFunction() { - // Function implementation -} +const myClass(); ``` **After applying the assist:** ```dart /// {@macro my_package.myFunction} -void myFunction() { - // Function implementation -} +const myClass(); ``` ### Wrap with macro template documentation comment @@ -380,9 +383,7 @@ When you select the documentation comment and execute **"Wrap with macro templat ```dart /// Some comment /// More comments -class MyClass { - // Class implementation -} +class MyClass {} ``` **After applying the assist:** @@ -392,11 +393,11 @@ class MyClass { /// Some comment /// More comments /// {@endtemplate} -class MyClass { - // Class implementation -} +class MyClass {} ``` +For class, enum, mixin, and extension type. + ## Lint rules adopted by altive_lints and why Reasons for adopting each lint rule. @@ -457,4 +458,4 @@ class DashboardCard extends StatelessWidget { The [public_member_api_docs](https://dart.dev/tools/linter-rules/public_member_api_docs) prompt to add documents has been activated. Please add documentation comments to public members. -If there are too many issues, disable them in analysis_options.yaml. \ No newline at end of file +If there are too many issues, disable them in analysis_options.yaml. diff --git a/packages/altive_lints/analysis_options.yaml b/packages/altive_lints/analysis_options.yaml index c419ba5..c213e44 100644 --- a/packages/altive_lints/analysis_options.yaml +++ b/packages/altive_lints/analysis_options.yaml @@ -1,6 +1,5 @@ include: lib/altive_lints.yaml -analyzer: - errors: - # Remove once we require analyzer 8.0.0 - # We currently support both 7.0 and 8.0, so we can't migrate yet. - deprecated_member_use: ignore +linter: + rules: + # use `test_` prefix in test_reflective_loader + non_constant_identifier_names: false diff --git a/packages/altive_lints/example/analysis_options.yaml b/packages/altive_lints/example/analysis_options.yaml index c3f27e2..e2010dc 100644 --- a/packages/altive_lints/example/analysis_options.yaml +++ b/packages/altive_lints/example/analysis_options.yaml @@ -1,11 +1,7 @@ # https://pub.dev/packages/altive_lints include: package:altive_lints/altive_lints.yaml -analyzer: - plugins: - - custom_lint linter: rules: # Disabled so that you don't have to write a doc since it is an `example`. - public_member_api_docs: false - diff --git a/packages/altive_lints/example/assists.dart b/packages/altive_lints/example/assists.dart new file mode 100644 index 0000000..0494695 --- /dev/null +++ b/packages/altive_lints/example/assists.dart @@ -0,0 +1,47 @@ +// This file is for testing the Assists feature of the Analyzer Plugin. + +import 'package:flutter/material.dart'; + +@immutable +class MyClass { + MyClass({required this.myField}); + + MyClass.emptyName() : myField = ''; + + final String myField; + + void myMethod() { + print('Hello, $myField!'); + } +} + +void myFunction() { + print('Hello, World!'); +} + +mixin MyMixin { + void myMixinMethod() { + print('Hello, World!'); + } +} + +enum MyEnum { + one, + two, + three, +} + +extension type MyExtensionType(int value) implements int { + bool get isZero => value == 0; +} + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + child: Text('Hello, World!'), + ); + } +} diff --git a/packages/altive_lints/example/lints/avoid_consecutive_sliver_to_box_adapter.dart b/packages/altive_lints/example/lints/avoid_consecutive_sliver_to_box_adapter.dart deleted file mode 100644 index b21fff3..0000000 --- a/packages/altive_lints/example/lints/avoid_consecutive_sliver_to_box_adapter.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConsecutiveSliverToBoxAdapters extends StatelessWidget { - const ConsecutiveSliverToBoxAdapters({super.key}); - - @override - Widget build(BuildContext context) { - return const CustomScrollView( - // expect_lint: avoid_consecutive_sliver_to_box_adapter - slivers: [ - SliverToBoxAdapter( - child: Text('Hello'), - ), - SliverToBoxAdapter( - child: Text('World'), - ), - ], - ); - } -} - -class ConsecutiveSliverToBoxAdaptersWithSliverPadding extends StatelessWidget { - const ConsecutiveSliverToBoxAdaptersWithSliverPadding({super.key}); - - @override - Widget build(BuildContext context) { - return const CustomScrollView( - // expect_lint: avoid_consecutive_sliver_to_box_adapter - slivers: [ - SliverPadding( - padding: EdgeInsets.zero, - sliver: SliverToBoxAdapter( - child: Text('Hello'), - ), - ), - SliverPadding( - padding: EdgeInsets.zero, - sliver: SliverToBoxAdapter( - child: Text('World'), - ), - ), - ], - ); - } -} - -class NonConsecutiveSliverToBoxAdapters extends StatelessWidget { - const NonConsecutiveSliverToBoxAdapters({super.key}); - - @override - Widget build(BuildContext context) { - return CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: Text('Hello'), - ), - SliverList.builder( - itemCount: 10, - itemBuilder: (context, index) { - return const SliverToBoxAdapter( - child: Text('item'), - ); - }, - ), - const SliverToBoxAdapter( - child: Text('World'), - ), - ], - ); - } -} diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_color.dart b/packages/altive_lints/example/lints/avoid_hardcoded_color.dart deleted file mode 100644 index 0be2bdf..0000000 --- a/packages/altive_lints/example/lints/avoid_hardcoded_color.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // expect_lint: avoid_hardcoded_color - const ColoredBox(color: Color(0xFF00FF00)), - // expect_lint: avoid_hardcoded_color - const ColoredBox(color: Color.fromRGBO(0, 255, 0, 1)), - // expect_lint: avoid_hardcoded_color - const ColoredBox(color: Colors.green), - ColoredBox(color: Theme.of(context).colorScheme.primary), - ColoredBox(color: _colorScheme.primary), - const ColoredBox(color: Colors.transparent), - ], - ); - } -} -ColorScheme get _colorScheme => const ColorScheme.dark( - primary:Color.fromRGBO(0, 255, 0, 1), - ); \ No newline at end of file diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_color_test.dart b/packages/altive_lints/example/lints/avoid_hardcoded_color_test.dart deleted file mode 100644 index 66c98ea..0000000 --- a/packages/altive_lints/example/lints/avoid_hardcoded_color_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Check the `avoid_hardcoded_color` rule. -// -// This file ends with the name `_test.dart`, -// so it should be exempt from the warning. - -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const ColoredBox(color: Color(0xFF00FF00)), - const ColoredBox(color: Color.fromRGBO(0, 255, 0, 1)), - const ColoredBox(color: Colors.green), - ColoredBox(color: Theme.of(context).colorScheme.primary), - ColoredBox(color: _colorScheme.primary), - const ColoredBox(color: Colors.transparent), - ], - ); - } -} - -ColorScheme get _colorScheme => const ColorScheme.dark( - primary: Color.fromRGBO(0, 255, 0, 1), - ); diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_japanese.dart b/packages/altive_lints/example/lints/avoid_hardcoded_japanese.dart deleted file mode 100644 index 816c9e9..0000000 --- a/packages/altive_lints/example/lints/avoid_hardcoded_japanese.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Check the `avoid_hardcoded_japanese` rule. - -// expect_lint: avoid_hardcoded_japanese -const hiragana = 'あいうえお'; - -// expect_lint: avoid_hardcoded_japanese -const katakana = 'アイウエオ'; - -// expect_lint: avoid_hardcoded_japanese -const kanji = '漢字'; - -const notJapanese = 'abc'; diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_japanese_test.dart b/packages/altive_lints/example/lints/avoid_hardcoded_japanese_test.dart deleted file mode 100644 index bd302cb..0000000 --- a/packages/altive_lints/example/lints/avoid_hardcoded_japanese_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Check the `avoid_hardcoded_japanese` rule. -// -// This file ends with the name `_test.dart`, -// so it should be exempt from the warning. - -const hiragana = 'あいうえお'; - -const katakana = 'アイウエオ'; - -const kanji = '漢字'; diff --git a/packages/altive_lints/example/lints/avoid_shrink_wrap_in_list_view.dart b/packages/altive_lints/example/lints/avoid_shrink_wrap_in_list_view.dart deleted file mode 100644 index 88204db..0000000 --- a/packages/altive_lints/example/lints/avoid_shrink_wrap_in_list_view.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: ListView( - children: [ - const Text('Hello World!'), - Expanded( - // expect_lint: avoid_shrink_wrap_in_list_view - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: const [ - Text('Hello'), - Text('World'), - ], - ), - ), - ], - ), - ); - } -} diff --git a/packages/altive_lints/example/lints/avoid_single_child.dart b/packages/altive_lints/example/lints/avoid_single_child.dart deleted file mode 100644 index 53df7c2..0000000 --- a/packages/altive_lints/example/lints/avoid_single_child.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - final isValued = Random().nextBool(); - final random = Random(); - return ListView( - children: [ - // expect_lint: avoid_single_child - const Column( - children: [ - Text('Hello'), - ], - ), - // expect_lint: avoid_single_child - const Row( - children: [ - Text('World'), - ], - ), - // expect_lint: avoid_single_child - const Flex( - direction: Axis.horizontal, - children: [ - Text('Hello'), - ], - ), - // expect_lint: avoid_single_child - const Wrap( - children: [ - Text('World'), - ], - ), - // expect_lint: avoid_single_child - const Stack( - children: [ - Text('Hello'), - ], - ), - // expect_lint: avoid_single_child - ListView( - children: const [ - Text('Hello'), - ], - ), - // expect_lint: avoid_single_child - SliverList.list( - children: const [ - Text('Hello'), - ], - ), - // expect_lint: avoid_single_child - SliverMainAxisGroup( - slivers: [ - SliverToBoxAdapter(child: Text('Hello')), - ], - ), - // expect_lint: avoid_single_child - SliverCrossAxisGroup( - slivers: [ - SliverToBoxAdapter(child: Text('Hello')), - ], - ), - // expect_lint: avoid_single_child - Column( - children: [ - if (random.nextBool()) const Text('Hello World'), - ], - ), - Column( - children: [ - if (random.nextBool()) - const Text('Hello World') - else ...[ - const Text('Hello'), - const Text('World'), - ], - ], - ), - // expect_lint: avoid_single_child - Column( - children: [ - if (isValued) ...[ - Container(), - ] else ...[ - Container(), - ], - ], - ), - Column( - children: [ - if (isValued) ...[ - Container(), - ], - Container(), - ], - ), - // expect_lint: avoid_single_child - Column( - children: [ - if (isValued) ...[ - Container(), - ], - ], - ), - Column( - children: [ - if (isValued) ...[ - Container(), - Container(), - ], - ], - ), - // expect_lint: avoid_single_child - Column( - children: [if (isValued) ...[] else ...[]], - ), - // expect_lint: avoid_single_child - Column( - children: [ - if (isValued) - ...[] - else ...[ - Container(), - ] - ], - ), - Column( - children: random.nextBool() - ? [ - const Text('Hello World'), - ] - : [ - const Text('Hello'), - const Text('World'), - ], - ), - if (random.nextBool()) - const Text('Hello World') - else - const Column( - children: [ - Text('Hello'), - Text('World'), - ], - ), - ListView( - children: List.generate( - 10, - (index) => Text('Hello $index'), - ), - ), - Row( - children: [ - for (var i = 0; i < 10; i++) Text('Hello $i'), - ], - ), - SliverList.list( - children: [ - for (final e in ['a', 'b', 'c']) Text('Hello $e'), - ], - ), - SliverMainAxisGroup( - slivers: [ - SliverAppBar( - title: const Text('Sliver App Bar'), - ), - SliverToBoxAdapter( - child: const Text('Hello World'), - ), - ], - ), - SliverCrossAxisGroup( - slivers: [ - SliverAppBar( - title: const Text('Sliver App Bar'), - ), - SliverToBoxAdapter( - child: const Text('Hello World'), - ), - ], - ), - Column( - children: [ - if (random.nextBool()) const Text('Hello 1'), - if (random.nextBool()) const Text('Hello 2'), - ], - ), - const Column(), - // Ignore this for the sake of example. - // ignore: avoid_redundant_argument_values - const Column(children: []), - ], - ); - } -} diff --git a/packages/altive_lints/example/lints/prefer_clock_now.dart b/packages/altive_lints/example/lints/prefer_clock_now.dart deleted file mode 100644 index 8210070..0000000 --- a/packages/altive_lints/example/lints/prefer_clock_now.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:clock/clock.dart'; - -// expect_lint: prefer_clock_now -final dateTimeNow = DateTime.now(); - -final clockNow = clock.now(); diff --git a/packages/altive_lints/example/lints/prefer_dedicated_media_query_methods.dart b/packages/altive_lints/example/lints/prefer_dedicated_media_query_methods.dart deleted file mode 100644 index b40654c..0000000 --- a/packages/altive_lints/example/lints/prefer_dedicated_media_query_methods.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - // expect_lint: prefer_dedicated_media_query_methods - final size = MediaQuery.of(context).size; - // expect_lint: prefer_dedicated_media_query_methods - final viewInsets = MediaQuery.of(context).viewInsets; - return Column( - children: [ - Text('Size: $size'), - Text('ViewInsets: $viewInsets'), - ], - ); - } -} diff --git a/packages/altive_lints/example/lints/prefer_space_between_elements.dart b/packages/altive_lints/example/lints/prefer_space_between_elements.dart deleted file mode 100644 index f14ff71..0000000 --- a/packages/altive_lints/example/lints/prefer_space_between_elements.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); // expect_lint: prefer_space_between_elements - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} - -class MyWidget2 extends StatelessWidget { - const MyWidget2({ - super.key, - required this.id, - }); // expect_lint: prefer_space_between_elements - final String id; // expect_lint: prefer_space_between_elements - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/packages/altive_lints/example/lints/prefer_to_include_sliver_in_name.dart b/packages/altive_lints/example/lints/prefer_to_include_sliver_in_name.dart deleted file mode 100644 index 6f5c496..0000000 --- a/packages/altive_lints/example/lints/prefer_to_include_sliver_in_name.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -// expect_lint: prefer_to_include_sliver_in_name -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - return SliverList.builder( - itemCount: 10, - itemBuilder: (context, index) { - return const Placeholder(); - }, - ); - } -} - -// expect_lint: prefer_to_include_sliver_in_name -class MyWidget2 extends StatelessWidget { - const MyWidget2({super.key, this.maxCount = 10}); - - const MyWidget2.small({super.key, this.maxCount = 5}); - - final int maxCount; - - @override - Widget build(BuildContext context) { - return SliverList.builder( - itemCount: maxCount, - itemBuilder: (context, index) { - return const Placeholder(); - }, - ); - } -} - -class MySliverWidget extends StatelessWidget { - const MySliverWidget({super.key}); - - @override - Widget build(BuildContext context) { - return SliverList.builder( - itemCount: 10, - itemBuilder: (context, index) { - return const Placeholder(); - }, - ); - } -} - -class MyWidget3 extends StatelessWidget { - const MyWidget3({super.key}); - - const MyWidget3.sliver({super.key}); - - @override - Widget build(BuildContext context) { - return SliverList.builder( - itemCount: 10, - itemBuilder: (context, index) { - return const Placeholder(); - }, - ); - } -} diff --git a/packages/altive_lints/example/pubspec.yaml b/packages/altive_lints/example/pubspec.yaml index 66cc25e..5fe48e3 100644 --- a/packages/altive_lints/example/pubspec.yaml +++ b/packages/altive_lints/example/pubspec.yaml @@ -3,7 +3,7 @@ description: A example of altive_lints. publish_to: none version: 1.0.0 environment: - sdk: ^3.0.0 + sdk: ^3.10.0 dependencies: flutter: @@ -13,4 +13,3 @@ dev_dependencies: altive_lints: path: ../ clock: ^1.1.2 - custom_lint: ^0.8.1 diff --git a/packages/altive_lints/example/rules.dart b/packages/altive_lints/example/rules.dart new file mode 100644 index 0000000..ecffa07 --- /dev/null +++ b/packages/altive_lints/example/rules.dart @@ -0,0 +1,122 @@ +// This file is intended to verify that Analysis rules are enabled. +// Please remove the ignore comment below +// if a warning appears, the check is working correctly. +// +// ignore_for_file: altive_lints/avoid_consecutive_sliver_to_box_adapter, altive_lints/avoid_hardcoded_color, altive_lints/avoid_hardcoded_japanese, altive_lints/avoid_shrink_wrap_in_list_view, altive_lints/avoid_single_child, altive_lints/prefer_clock_now, altive_lints/prefer_dedicated_media_query_methods, altive_lints/prefer_space_between_elements, altive_lints/prefer_to_include_sliver_in_name +import 'package:flutter/material.dart'; + +class AvoidConsecutiveSliverToBoxAdaptersExample extends StatelessWidget { + const AvoidConsecutiveSliverToBoxAdaptersExample({super.key}); + + @override + Widget build(BuildContext context) { + return const CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.zero, + sliver: SliverToBoxAdapter( + child: Text('Hello'), + ), + ), + SliverPadding( + padding: EdgeInsets.zero, + sliver: SliverToBoxAdapter( + child: Text('World'), + ), + ), + ], + ); + } +} + +class AvoidHardcodedColorExample extends StatelessWidget { + const AvoidHardcodedColorExample({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const ColoredBox(color: Color(0xFF00FF00)), + const ColoredBox(color: Colors.green), + ], + ); + } +} + +const hiragana = 'あいうえお'; + +class AvoidShrinkWrapInListViewExample extends StatelessWidget { + const AvoidShrinkWrapInListViewExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + children: [ + const Text('Hello World!'), + Expanded( + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: const [ + Text('Hello'), + Text('World'), + ], + ), + ), + ], + ), + ); + } +} + +class AvoidSingleChildExample extends StatelessWidget { + const AvoidSingleChildExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Text('Hello'), + ], + ); + } +} + +final dateTimeNow = DateTime.now(); + +class PreferDedicatedMediaQueryMethodsExample extends StatelessWidget { + const PreferDedicatedMediaQueryMethodsExample({super.key}); + + @override + Widget build(BuildContext context) { + final _ = MediaQuery.of(context).size; + return const SizedBox.shrink(); + } +} + +class PreferSpaceBetweenElementsExample extends StatelessWidget { + const PreferSpaceBetweenElementsExample({ + super.key, + required this.id, + }); + final String id; + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} + +class PreferToIncludeFooInNameExample extends StatelessWidget { + const PreferToIncludeFooInNameExample({super.key}); + + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemCount: 10, + itemBuilder: (context, index) { + return const Placeholder(); + }, + ); + } +} diff --git a/packages/altive_lints/example/test/avoid_hardcoded_color.dart b/packages/altive_lints/example/test/avoid_hardcoded_color.dart deleted file mode 100644 index 9afecd8..0000000 --- a/packages/altive_lints/example/test/avoid_hardcoded_color.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Check the `avoid_hardcoded_color` rule. -// -// It should exclude warnings for the entire `test` directory. - -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const ColoredBox(color: Color(0xFF00FF00)), - const ColoredBox(color: Color.fromRGBO(0, 255, 0, 1)), - const ColoredBox(color: Colors.green), - ColoredBox(color: Theme.of(context).colorScheme.primary), - ColoredBox(color: _colorScheme.primary), - const ColoredBox(color: Colors.transparent), - ], - ); - } -} - -ColorScheme get _colorScheme => const ColorScheme.dark( - primary: Color.fromRGBO(0, 255, 0, 1), - ); diff --git a/packages/altive_lints/example/test/avoid_hardcoded_japanese.dart b/packages/altive_lints/example/test/avoid_hardcoded_japanese.dart deleted file mode 100644 index 95785f1..0000000 --- a/packages/altive_lints/example/test/avoid_hardcoded_japanese.dart +++ /dev/null @@ -1,9 +0,0 @@ -// Check the `avoid_hardcoded_japanese` rule. -// -// It should exclude warnings for the entire `test` directory. - -const hiragana = 'あいうえお'; - -const katakana = 'アイウエオ'; - -const kanji = '漢字'; diff --git a/packages/altive_lints/lib/altive_lints.dart b/packages/altive_lints/lib/altive_lints.dart deleted file mode 100644 index e8591d9..0000000 --- a/packages/altive_lints/lib/altive_lints.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:custom_lint_builder/custom_lint_builder.dart'; - -import 'src/assists/add_macro_document_comments.dart'; -import 'src/assists/add_macro_template_document_comment.dart'; -import 'src/assists/wrap_with_macro_template_document_comment.dart'; -import 'src/lints/avoid_consecutive_sliver_to_box_adapter.dart'; -import 'src/lints/avoid_hardcoded_color.dart'; -import 'src/lints/avoid_hardcoded_japanese.dart'; -import 'src/lints/avoid_shrink_wrap_in_list_view.dart'; -import 'src/lints/avoid_single_child.dart'; -import 'src/lints/prefer_clock_now.dart'; -import 'src/lints/prefer_dedicated_media_query_methods.dart'; -import 'src/lints/prefer_space_between_elements.dart'; -import 'src/lints/prefer_to_include_sliver_in_name.dart'; - -/// Returns the Altive Plugin instance. -PluginBase createPlugin() => _AltivePlugin(); - -class _AltivePlugin extends PluginBase { - @override - List getLintRules(CustomLintConfigs configs) => [ - const AvoidConsecutiveSliverToBoxAdapter(), - const AvoidHardcodedColor(), - const AvoidHardcodedJapanese(), - const AvoidShrinkWrapInListView(), - const AvoidSingleChild(), - const PreferClockNow(), - const PreferDedicatedMediaQueryMethods(), - const PreferSpaceBetweenElements(), - const PreferToIncludeSliverInName(), - ]; - - @override - List getAssists() => [ - AddMacroDocumentComment(), - AddMacroTemplateDocumentComment(), - WrapWithMacroTemplateDocumentComment(), - ]; -} diff --git a/packages/altive_lints/lib/altive_lints.yaml b/packages/altive_lints/lib/altive_lints.yaml index ee73c97..4f0e833 100644 --- a/packages/altive_lints/lib/altive_lints.yaml +++ b/packages/altive_lints/lib/altive_lints.yaml @@ -14,6 +14,10 @@ formatter: # The default setting conflicts with require_trailing_commas. trailing_commas: preserve +plugins: + altive_lints: + path: ../../altive_lints + linter: rules: # Conflicts with the convention used by flutter, which puts `Key key` diff --git a/packages/altive_lints/lib/main.dart b/packages/altive_lints/lib/main.dart new file mode 100644 index 0000000..3c096bf --- /dev/null +++ b/packages/altive_lints/lib/main.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:analysis_server_plugin/plugin.dart'; +import 'package:analysis_server_plugin/registry.dart'; + +import 'src/assists/add_macro_document_comments.dart'; +import 'src/assists/add_macro_template_document_comment.dart'; +import 'src/assists/wrap_with_macro_template_document_comment.dart'; +import 'src/lints/avoid_consecutive_sliver_to_box_adapter.dart'; +import 'src/lints/avoid_hardcoded_color.dart'; +import 'src/lints/avoid_hardcoded_japanese.dart'; +import 'src/lints/avoid_shrink_wrap_in_list_view.dart'; +import 'src/lints/avoid_single_child.dart'; +import 'src/lints/prefer_clock_now.dart'; +import 'src/lints/prefer_dedicated_media_query_methods.dart'; +import 'src/lints/prefer_space_between_elements.dart'; +import 'src/lints/prefer_to_include_sliver_in_name.dart'; + +/// Enables Altive lints. +final plugin = _Plugin(); + +class _Plugin extends Plugin { + @override + String get name => 'altive_lints'; + + @override + Future register(PluginRegistry registry) async { + registry + ..registerWarningRule(AvoidConsecutiveSliverToBoxAdapter()) + ..registerWarningRule(AvoidHardcodedColor()) + ..registerWarningRule(AvoidHardcodedJapanese()) + ..registerWarningRule(AvoidShrinkWrapInListView()) + ..registerWarningRule(AvoidSingleChild()) + ..registerWarningRule(PreferClockNow()) + ..registerWarningRule(PreferDedicatedMediaQueryMethods()) + ..registerWarningRule(PreferSpaceBetweenElements()) + ..registerWarningRule(PreferToIncludeSliverInName()) + ..registerAssist(AddMacroDocumentComment.new) + ..registerAssist(AddMacroTemplateDocumentComment.new) + ..registerAssist(WrapWithMacroTemplateDocumentComment.new) + /* */; + } +} diff --git a/packages/altive_lints/lib/src/assists/add_macro_document_comments.dart b/packages/altive_lints/lib/src/assists/add_macro_document_comments.dart index 8b456a4..4dc380e 100644 --- a/packages/altive_lints/lib/src/assists/add_macro_document_comments.dart +++ b/packages/altive_lints/lib/src/assists/add_macro_document_comments.dart @@ -1,6 +1,10 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; + +import '../utils/package_utils.dart'; /// {@template altive_lints.AddMacroDocumentComment} /// An assist to add macro template documentation comments to function @@ -14,7 +18,7 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// Macro template comments follow this format: /// /// ```dart -/// /// {[@]macro packageName.functionName} +/// /// {@macro packageName.functionName} /// ``` /// /// Example: @@ -28,61 +32,86 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// /// After applying the assist: /// ```dart -/// /// {[@]macro my_package.myFunction} +/// /// {@macro my_package.myFunction} /// void myFunction() { /// // Function implementation /// } /// ``` /// /// {@endtemplate} -class AddMacroDocumentComment extends DartAssist { +class AddMacroDocumentComment extends ResolvedCorrectionProducer { /// {@macro altive_lints.AddMacroDocumentComment} - AddMacroDocumentComment(); + AddMacroDocumentComment({required super.context}); + + static const _kind = AssistKind( + 'dart.assist.addMacroDocumentComment', + DartFixKindPriority.standard, + 'Add a macro document comment', + ); @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - SourceRange target, - ) { - context.registry.addFunctionDeclaration((node) { - _processNode(node, target, reporter, context); - }); + CorrectionApplicability get applicability => .singleLocation; - context.registry.addConstructorDeclaration((node) { - _processNode(node, target, reporter, context); - }); - } + @override + AssistKind get assistKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + final node = this.node; + + // Locate the ancestor ClassMember (covers both Methods and Constructors). + final member = node.thisOrAncestorOfType(); + + if (member == null) { + return; + } + + // Extract necessary data using Dart 3 pattern matching. + // This unifies the logic for MethodDeclaration and ConstructorDeclaration. + final ( + String? nameSuffix, + FunctionBody? body, + Comment? existingDoc, + ) = switch (member) { + MethodDeclaration m => (m.name.lexeme, m.body, m.documentationComment), + ConstructorDeclaration c => ( + c.name?.lexeme, + c.body, + c.documentationComment, + ), + // Ignore other class members like fields for now. + _ => (null, null, null), + }; + + // If it's not a method or constructor, or if it already has docs, exit. + if (body == null || existingDoc != null) { + return; + } + + // Validate cursor position. Ensure the cursor is strictly before the body + // starts (on the signature line). + if (selectionOffset >= body.offset) { + return; + } - void _processNode( - AstNode node, - SourceRange target, - ChangeReporter reporter, - CustomLintContext context, - ) { - if (!target.intersects(node.sourceRange)) { + final classDeclaration = member.thisOrAncestorOfType(); + if (classDeclaration == null) { return; } - final changeBuilder = reporter.createChangeBuilder( - message: 'Add macro documentation comment', - priority: 20, - ); + // --- Generation Logic --- - final packageName = context.pubspec.name; - var name = ''; + final className = classDeclaration.name.lexeme; - if (node is FunctionDeclaration) { - name = node.name.lexeme; - } else if (node is ConstructorDeclaration) { - name = node.returnType.name; + var macroReference = '$packageName.$className'; + if (nameSuffix != null && nameSuffix.isNotEmpty) { + macroReference += '.$nameSuffix'; } - final macroComment = '/// {@macro $packageName.$name}'; + final macroComment = '/// {@macro $macroReference}'; - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleInsertion(node.offset, '$macroComment\n'); + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleInsertion(member.offset, '$macroComment\n'); }); } } diff --git a/packages/altive_lints/lib/src/assists/add_macro_template_document_comment.dart b/packages/altive_lints/lib/src/assists/add_macro_template_document_comment.dart index 43896f3..9b24cb9 100644 --- a/packages/altive_lints/lib/src/assists/add_macro_template_document_comment.dart +++ b/packages/altive_lints/lib/src/assists/add_macro_template_document_comment.dart @@ -1,5 +1,10 @@ -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; + +import '../utils/package_utils.dart'; /// {@template altive_lints.AddMacroTemplateDocumentComment} /// A Dart assist that adds a macro template documentation comment to a class @@ -12,9 +17,9 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// The macro template comment follows the format: /// /// ```dart -/// /// {[@]template packageName.className} +/// /// {@template packageName.className} /// /// -/// /// {[@]endtemplate} +/// /// {@endtemplate} /// ``` /// /// Example usage: @@ -28,53 +33,68 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// /// After running the assist: /// ```dart -/// /// {[@]template my_package.MyClass} +/// /// {@template my_package.MyClass} /// /// -/// /// {[@]endtemplate} +/// /// {@endtemplate} /// class MyClass { /// // Class implementation /// } /// ``` /// /// {@endtemplate} -class AddMacroTemplateDocumentComment extends DartAssist { +class AddMacroTemplateDocumentComment extends ResolvedCorrectionProducer { /// {@macro altive_lints.AddMacroTemplateDocumentComment} - AddMacroTemplateDocumentComment(); + AddMacroTemplateDocumentComment({required super.context}); + + static const _kind = AssistKind( + 'dart.assist.addMacroTemplateDocumentComment', + DartFixKindPriority.standard, + 'Add a macro template documentation comment', + ); @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - SourceRange target, - ) { - context.registry.addClassDeclaration((node) { - if (!target.intersects(node.sourceRange)) { - return; - } + CorrectionApplicability get applicability => .singleLocation; + + @override + AssistKind get assistKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + final node = this.node; + + // Locate the ancestor declaration (Class, Mixin, Enum, etc.) + final declaration = node.thisOrAncestorOfType(); + if (declaration == null) { + return; + } + + // Check for existing docs. + if (declaration.documentationComment != null) { + return; + } + + // Validate cursor position (Must be on the name). + final nameToken = declaration.name; - final docComment = node.documentationComment; - if (docComment != null) { - return; - } + // Check if cursor is strictly within the name token range. + if (selectionOffset < nameToken.offset || selectionOffset > nameToken.end) { + return; + } - final changeBuilder = reporter.createChangeBuilder( - message: 'Add macro template documentation comment', - priority: 20, - ); + // --- Generation Logic --- - final packageName = context.pubspec.name; - final className = node.name.lexeme; + final name = nameToken.lexeme; - final template = [ - '/// {@template $packageName.$className}', - '/// ', - '/// {@endtemplate}', - ].join('\n'); + final template = [ + '/// {@template $packageName.$name}', + '/// ', + '/// {@endtemplate}', + ].join('\n'); - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleInsertion(node.offset, '$template\n'); - }); + await builder.addDartFileEdit(file, (builder) { + // Insert at the very beginning of the declaration (before annotations), + // not at the cursor position (node.offset). + builder.addSimpleInsertion(declaration.offset, '$template\n'); }); } } diff --git a/packages/altive_lints/lib/src/assists/wrap_with_macro_template_document_comment.dart b/packages/altive_lints/lib/src/assists/wrap_with_macro_template_document_comment.dart index c71d459..5950e88 100644 --- a/packages/altive_lints/lib/src/assists/wrap_with_macro_template_document_comment.dart +++ b/packages/altive_lints/lib/src/assists/wrap_with_macro_template_document_comment.dart @@ -1,6 +1,10 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; + +import '../utils/package_utils.dart'; /// {@template altive_lints.WrapWithMacroTemplateDocumentComment} /// A Dart assist that wraps an existing documentation comment with a macro @@ -14,9 +18,9 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// The macro template comment follows the format: /// /// ```dart -/// /// {[@]template packageName.className} +/// /// {@template packageName.className} /// /// Existing documentation comment. -/// /// {[@]endtemplate} +/// /// {@endtemplate} /// ``` /// /// Example usage: @@ -31,57 +35,82 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// /// After running the assist: /// ```dart -/// /// {[@]template my_package.MyClass} +/// /// {@template my_package.MyClass} /// /// This is a class. -/// /// {[@]endtemplate} +/// /// {@endtemplate} /// class MyClass { /// // Class implementation /// } /// ``` /// /// {@endtemplate} -class WrapWithMacroTemplateDocumentComment extends DartAssist { +class WrapWithMacroTemplateDocumentComment extends ResolvedCorrectionProducer { /// {@macro altive_lints.WrapWithMacroTemplateDocumentComment} - WrapWithMacroTemplateDocumentComment(); + WrapWithMacroTemplateDocumentComment({required super.context}); + + static const _kind = AssistKind( + 'dart.assist.wrapWithMacroTemplateDocumentComment', + DartFixKindPriority.standard, + 'Wrap with a macro template documentation comment', + ); @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - SourceRange target, - ) { - context.registry.addComment((node) { - if (!target.intersects(node.sourceRange)) { - return; - } + CorrectionApplicability get applicability => .singleLocation; + + @override + AssistKind get assistKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + final node = this.node; + + // Locate the ancestor declaration (Class, Mixin, Enum, etc.) + final declaration = node.thisOrAncestorOfType(); + if (declaration == null) { + return; + } + + // Get the documentation comment directly from the declaration + final comment = declaration.documentationComment; + if (comment == null) { + return; + } + + // Check cursor position validity + // Allow execution if cursor is: + // A) Inside the documentation comment + // B) On the class/member name (UX improvement) + final selectionInComment = + selectionOffset >= comment.offset && selectionOffset <= comment.end; - final currentComment = node.tokens.join(); - if (currentComment.contains('{@template') && - currentComment.contains('{@endtemplate}')) { + final nameToken = declaration.name; + final selectionOnName = + selectionOffset >= nameToken.offset && selectionOffset <= nameToken.end; + + if (!selectionInComment && !selectionOnName) { + return; + } + + // Prevent double wrapping + for (final token in comment.tokens) { + if (token.lexeme.contains('{@template')) { return; } + } - final changeBuilder = reporter.createChangeBuilder( - message: 'Wrap with macro template documentation comment', - priority: 20, - ); + // --- Generation Logic --- - final packageName = context.pubspec.name; - final classNode = node.parent; - var className = ''; - if (classNode is ClassDeclaration) { - className = classNode.name.lexeme; - } + final className = declaration.name.lexeme; - final templateStart = '/// {@template $packageName.$className}'; - const templateEnd = '/// {@endtemplate}'; + final templateStart = '/// {@template $packageName.$className}'; + const templateEnd = '/// {@endtemplate}'; - changeBuilder.addDartFileEdit((builder) { - builder - ..addSimpleInsertion(node.offset, '$templateStart\n') - ..addSimpleInsertion(node.end, '\n$templateEnd'); - }); + await builder.addDartFileEdit(file, (builder) { + builder + // Insert header at the very beginning of the existing comment + ..addSimpleInsertion(comment.offset, '$templateStart\n') + // Insert footer at the very end of the existing comment + ..addSimpleInsertion(comment.end, '\n$templateEnd'); }); } } diff --git a/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart index f28f90c..1aa54b3 100644 --- a/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart +++ b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart @@ -1,6 +1,9 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; /// A `avoid_consecutive_sliver_to_box_adapter` rule that /// identifies and discourages the use of consecutive @@ -40,39 +43,53 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// ], /// ); /// ``` -class AvoidConsecutiveSliverToBoxAdapter extends DartLintRule { +class AvoidConsecutiveSliverToBoxAdapter extends AnalysisRule { /// Creates a new instance of [AvoidConsecutiveSliverToBoxAdapter]. - const AvoidConsecutiveSliverToBoxAdapter() : super(code: _code); + AvoidConsecutiveSliverToBoxAdapter() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'avoid_consecutive_sliver_to_box_adapter', - problemMessage: 'Avoid using consecutive `SliverToBoxAdapter`. ' + 'avoid_consecutive_sliver_to_box_adapter', + 'Avoid using consecutive `SliverToBoxAdapter`. ' 'Consider using `SliverList.list` instead.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addListLiteral((node) { - final iterator = node.elements.iterator; - if (!iterator.moveNext()) { - // if there are no elements, there is nothing to check. - return; - } + final visitor = _Visitor(this, context); + registry.addListLiteral(this, visitor); + } +} - var current = iterator.current; - while (iterator.moveNext()) { - final next = iterator.current; - if (_useSliverToBoxAdapter(current) && _useSliverToBoxAdapter(next)) { - reporter.atNode(node, _code); - return; - } - current = next; +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitListLiteral(ListLiteral node) { + final iterator = node.elements.iterator; + if (!iterator.moveNext()) { + // if there are no elements, there is nothing to check. + return; + } + + var current = iterator.current; + while (iterator.moveNext()) { + final next = iterator.current; + if (_useSliverToBoxAdapter(current) && _useSliverToBoxAdapter(next)) { + rule.reportAtNode(node); + return; } - }); + current = next; + } } bool _useSliverToBoxAdapter(CollectionElement element) { diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart index 7370b62..912a8cb 100644 --- a/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart @@ -1,11 +1,15 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/element2.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/error/error.dart'; import '../utils/files_utils.dart'; +/// {@template altive_lints.AvoidHardcodedColor} /// An `avoid_hardcoded_color` rule that discourages the use of /// hardcoded colors directly in the code, promoting the use of `ColorScheme`, /// `ThemeExtension`, or other Theme-based systems for defining colors. @@ -30,70 +34,155 @@ import '../utils/files_utils.dart'; /// color: Theme.of(context).colorScheme.primary, /// ); /// ``` -class AvoidHardcodedColor extends DartLintRule { - /// Creates a new instance of [AvoidHardcodedColor]. - const AvoidHardcodedColor() : super(code: _code); +/// {@endtemplate} +class AvoidHardcodedColor extends AnalysisRule { + /// {@macro altive_lints.AvoidHardcodedColor} + AvoidHardcodedColor() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'avoid_hardcoded_color', - problemMessage: 'Avoid using hardcoded color. Use ColorScheme, ' - 'ThemeExtension, or other Theme-based color definitions instead. ' - 'Consider using Theme.of(context).colorScheme or a custom ' - 'ThemeExtension for color definitions.', + 'avoid_hardcoded_color', + 'Avoid using hardcoded color. Use ColorScheme based definitions.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - if (isTestFile(resolver.source)) { + final visitor = _Visitor(this, context); + registry + ..addInstanceCreationExpression(this, visitor) + ..addMethodInvocation(this, visitor) + ..addPrefixedIdentifier(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + bool get _isTestFile => isTestFile(context.definingUnit.file); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (_isTestFile || _isInsideColorScheme(node)) { + return; + } + + // constructorName.staticElement が存在しない場合があるため、 + // 生成される型 (staticType) からクラス要素を取得する方法に変更 + final type = node.staticType; + if (_isColorClass(type?.element)) { + rule.reportAtNode(node); + } + } + + @override + void visitMethodInvocation(MethodInvocation node) { + if (_isTestFile || _isInsideColorScheme(node)) { return; } - context.registry.addInstanceCreationExpression((node) { - if (_isInsideColorScheme(node)) { - return; + + // methodName.staticElement -> methodName.element (互換性のため) + final element = node.methodName.element; + + // コンストラクタ呼び出しの場合のチェック + if (element is ConstructorElement) { + // enclosingElement3 -> enclosingElement (互換性のため) + if (_isColorClass(element.enclosingElement)) { + rule.reportAtNode(node); } - final typeName = node.staticType?.getDisplayString(); + return; + } - if (typeName == 'Color') { - reporter.atNode(node, _code); + // 静的メソッド (Color.fromARGBなど) の場合 + if (element is MethodElement && element.isStatic) { + if (_isColorClass(element.enclosingElement)) { + rule.reportAtNode(node); } - }); - - context.registry.addPrefixedIdentifier((node) { - final prefix = node.prefix.name; - if (prefix == 'Colors') { - final element = node.element; - if (element is PropertyAccessorElement2) { - final returnType = element.returnType; - // Allow Colors.transparent as a valid hardcoded color, as it serves. - if (node.identifier.name == 'transparent') { - return; - } - if (_isColorType(returnType)) { - reporter.atNode(node, _code); - } + } + } + + @override + void visitPrefixedIdentifier(PrefixedIdentifier node) { + if (_isTestFile || _isInsideColorScheme(node)) { + return; + } + + final element = node.element; + + if (element is PropertyAccessorElement || element is FieldElement) { + // enclosingElement3 -> enclosingElement + final parentClass = element?.enclosingElement; + if (parentClass is ClassElement && parentClass.name == 'Colors') { + // Colors.transparent is allowed + if (node.identifier.name == 'transparent') { + return; + } + + final type = (element is PropertyAccessorElement) + ? element.returnType + : (element! as FieldElement).type; + + if (_isColorType(type)) { + rule.reportAtNode(node); } } - }); + } + } + + bool _isColorClass(Element? element) { + if (element is! ClassElement) { + return false; + } + return element.name == 'Color' || + element.name == 'MaterialColor' || + element.name == 'MaterialAccentColor'; } bool _isColorType(DartType? type) { - return type != null && - (type.isDartCoreInt || - type.getDisplayString() == 'Color' || - type.getDisplayString() == 'MaterialColor' || - type.getDisplayString() == 'MaterialAccentColor'); + if (type == null) { + return false; + } + if (type.isDartCoreInt) { + return false; + } + return _isColorClass(type.element); } + /// Checks if the node is defined inside a ColorScheme context. bool _isInsideColorScheme(AstNode node) { var parent = node.parent; while (parent != null) { - if (parent is InstanceCreationExpression && - parent.staticType?.getDisplayString() == 'ColorScheme') { - return true; + // 1. Check if we are inside a ColorScheme(...) constructor + if (parent is InstanceCreationExpression) { + final type = parent.staticType; + // type.element は古いバージョンでも存在します + if (type != null && type.element?.name == 'ColorScheme') { + return true; + } + } + + // 2. Check if we are inside a method call on a ColorScheme object + if (parent is MethodInvocation) { + final targetType = parent.target?.staticType; + if (targetType != null && targetType.element?.name == 'ColorScheme') { + return true; + } + + // Fallback + // methodName.staticElement -> methodName.element + final methodElement = parent.methodName.element; + // enclosingElement3 -> enclosingElement + if (methodElement?.enclosingElement?.name == 'ColorScheme') { + return true; + } } parent = parent.parent; } diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart index acaa11a..cb681b3 100644 --- a/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart @@ -1,5 +1,9 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; import '../utils/files_utils.dart'; @@ -25,46 +29,67 @@ import '../utils/files_utils.dart'; /// print(AppLocalizations.of(context).errorOccurred); /// ``` /// -class AvoidHardcodedJapanese extends DartLintRule { +class AvoidHardcodedJapanese extends AnalysisRule { /// Creates a new instance of [AvoidHardcodedJapanese]. - const AvoidHardcodedJapanese() : super(code: _code); + AvoidHardcodedJapanese() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'avoid_hardcoded_japanese', - problemMessage: 'This string appears to be untranslated to Japanese.\n' + 'avoid_hardcoded_japanese', + 'This string appears to be untranslated to Japanese.\n' 'Ensure all user-facing text is properly internationalized for ' 'Japanese localization.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - if (isTestFile(resolver.source)) { + final visitor = _Visitor(this, context); + registry.addCompilationUnit(this, visitor); + } +} + +class _Visitor extends RecursiveAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + bool get _isTestFile => isTestFile(context.definingUnit.file); + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (_isTestFile) { + super.visitSimpleStringLiteral(node); return; } - context.registry.addSimpleStringLiteral((node) { - final stringValue = node.stringValue; - if (stringValue == null) { - return; - } - if (isJapanese(stringValue)) { - reporter.atNode(node, _code); - } - }); + final stringValue = node.stringValue; + if (stringValue != null && _isJapanese(stringValue)) { + rule.reportAtNode(node); + } + super.visitSimpleStringLiteral(node); + } - context.registry.addStringInterpolation((node) { - final stringValue = node.toSource(); - if (isJapanese(stringValue)) { - reporter.atNode(node, _code); - } - }); + @override + void visitStringInterpolation(StringInterpolation node) { + if (_isTestFile) { + super.visitStringInterpolation(node); + return; + } + final stringValue = node.toSource(); + if (_isJapanese(stringValue)) { + rule.reportAtNode(node); + } + super.visitStringInterpolation(node); } /// Checks if the string contains Japanese characters /// (Hiragana, Katakana, Kanji). - bool isJapanese(String value) => + bool _isJapanese(String value) => RegExp(r'[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FD0]').hasMatch(value); } diff --git a/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart b/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart index 5690d38..0a6d10e 100644 --- a/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart +++ b/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart @@ -1,7 +1,10 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; import '../utils/types_utils.dart'; @@ -41,13 +44,14 @@ import '../utils/types_utils.dart'; /// ], /// ); /// ``` -class AvoidShrinkWrapInListView extends DartLintRule { +class AvoidShrinkWrapInListView extends AnalysisRule { /// Creates a new instance of [AvoidShrinkWrapInListView]. - const AvoidShrinkWrapInListView() : super(code: _code); + AvoidShrinkWrapInListView() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'avoid_shrink_wrap_in_list_view', - problemMessage: 'Avoid using ListView with shrinkWrap, ' + 'avoid_shrink_wrap_in_list_view', + 'Avoid using ListView with shrinkWrap, ' 'since it might degrade the performance.\n' 'Consider using slivers instead. ' 'Or, it is originally intended to be used for shrinking ' @@ -55,18 +59,31 @@ class AvoidShrinkWrapInListView extends DartLintRule { ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addInstanceCreationExpression((node) { - if (isListViewWidget(node.staticType) && - _hasShrinkWrap(node) && - _hasParentList(node)) { - reporter.atNode(node, _code); - } - }); + final visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + if (isListViewWidget(node.staticType) && + _hasShrinkWrap(node) && + _hasParentList(node)) { + rule.reportAtNode(node); + } } bool _hasShrinkWrap(InstanceCreationExpression node) => diff --git a/packages/altive_lints/lib/src/lints/avoid_single_child.dart b/packages/altive_lints/lib/src/lints/avoid_single_child.dart index 5b65634..b165038 100644 --- a/packages/altive_lints/lib/src/lints/avoid_single_child.dart +++ b/packages/altive_lints/lib/src/lints/avoid_single_child.dart @@ -1,7 +1,10 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; /// An `avoid_single_child` rule that warns against using layout /// widgets intended for multiple children with only one child. @@ -33,73 +36,86 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// children: [YourWidget1(), YourWidget2()], /// ); /// ``` -class AvoidSingleChild extends DartLintRule { +class AvoidSingleChild extends AnalysisRule { /// Creates a new instance of [AvoidSingleChild]. - const AvoidSingleChild() : super(code: _code); + AvoidSingleChild() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'avoid_single_child', - problemMessage: - 'Avoid using a single child in widgets that expect multiple children. ' + 'avoid_single_child', + 'Avoid using a single child in widgets that expect multiple children. ' 'Consider using a single child widget or adding more children.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addInstanceCreationExpression((node) { - final className = node.staticType?.getDisplayString(); - if ([ - 'Column', - 'Row', - 'Flex', - 'Wrap', - 'Stack', - 'ListView', - 'SliverList', - 'SliverMainAxisGroup', - 'SliverCrossAxisGroup', - ].contains(className)) { - final childrenArg = node.argumentList.arguments.firstWhereOrNull( - (arg) => - arg is NamedExpression && - (arg.name.label.name == 'children' || - arg.name.label.name == 'slivers'), - ); + final visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} - final ListLiteral childrenList; - if (childrenArg is NamedExpression && - childrenArg.expression is ListLiteral) { - childrenList = childrenArg.expression as ListLiteral; - } else { - return; - } +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); - if (childrenList.elements.length != 1) { - return; - } - for (final element in childrenList.elements) { - if (element is IfElement) { - if (_hasMultipleChild(element.thenElement)) { - return; - } + final AnalysisRule rule; + final RuleContext context; + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + final className = node.staticType?.getDisplayString(); + if ([ + 'Column', + 'Row', + 'Flex', + 'Wrap', + 'Stack', + 'ListView', + 'SliverList', + 'SliverMainAxisGroup', + 'SliverCrossAxisGroup', + ].contains(className)) { + final childrenArg = node.argumentList.arguments.firstWhereOrNull( + (arg) => + arg is NamedExpression && + (arg.name.label.name == 'children' || + arg.name.label.name == 'slivers'), + ); - if (element.elseElement case final CollectionElement ce - when _hasMultipleChild(ce)) { - return; - } + final ListLiteral childrenList; + if (childrenArg is NamedExpression && + childrenArg.expression is ListLiteral) { + childrenList = childrenArg.expression as ListLiteral; + } else { + return; + } + + if (childrenList.elements.length != 1) { + return; + } + for (final element in childrenList.elements) { + if (element is IfElement) { + if (_hasMultipleChild(element.thenElement)) { + return; + } + + if (element.elseElement case final CollectionElement ce + when _hasMultipleChild(ce)) { + return; } } - final element = childrenList.elements.first; - if (element is ForElement) { - return; - } - reporter.atNode(node, _code); } - }); + final element = childrenList.elements.first; + if (element is ForElement) { + return; + } + rule.reportAtNode(node); + } } bool _hasMultipleChild(CollectionElement element) { diff --git a/packages/altive_lints/lib/src/lints/prefer_clock_now.dart b/packages/altive_lints/lib/src/lints/prefer_clock_now.dart index 82e8028..07f6dbe 100644 --- a/packages/altive_lints/lib/src/lints/prefer_clock_now.dart +++ b/packages/altive_lints/lib/src/lints/prefer_clock_now.dart @@ -1,5 +1,9 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; /// A `prefer_clock_now` rule that discourages the use of /// `DateTime.now()` due to its non-testability in unit tests. @@ -21,31 +25,45 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// ```dart /// var now = clock.now(); // Using 'clock' package /// ``` -class PreferClockNow extends DartLintRule { - /// Creates a new instance of [PreferClockNow]. - const PreferClockNow() : super(code: _code); +class PreferClockNow extends AnalysisRule { + /// + PreferClockNow() : super(name: _code.name, description: _code.problemMessage); + /// static const _code = LintCode( - name: 'prefer_clock_now', - problemMessage: 'Avoid using DateTime.now(). ' + 'prefer_clock_now', + 'Avoid using DateTime.now(). ' 'Use a testable alternative like clock.now() or similar instead.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addInstanceCreationExpression((node) { - final constructorName = node.constructorName; - final type = constructorName.type.name2.lexeme; - if (type != 'DateTime') { - return; - } - if (node.constructorName.name?.name == 'now') { - reporter.atNode(node, _code); - } - }); + final visitor = _Visitor(this, context); + registry.addInstanceCreationExpression(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + final constructorName = node.constructorName; + final type = constructorName.type.name.lexeme; + if (type != 'DateTime') { + return; + } + if (node.constructorName.name?.name == 'now') { + rule.reportAtNode(node); + } } } diff --git a/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart b/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart index c645aa8..e36ad8b 100644 --- a/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart +++ b/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart @@ -1,5 +1,9 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; /// A `prefer_dedicated_media_query_methods` rule that encourages /// the use of dedicated `MediaQuery` methods instead of @@ -28,30 +32,65 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// var size = MediaQuery.sizeOf(context); /// var padding = MediaQuery.viewInsetsOf(context); /// ``` -class PreferDedicatedMediaQueryMethods extends DartLintRule { +class PreferDedicatedMediaQueryMethods extends AnalysisRule { /// Creates a new instance of [PreferDedicatedMediaQueryMethods]. - const PreferDedicatedMediaQueryMethods() : super(code: _code); + PreferDedicatedMediaQueryMethods() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'prefer_dedicated_media_query_methods', - problemMessage: 'Prefer using dedicated MediaQuery methods instead of ' - 'MediaQuery.of or MediaQuery.maybeOf.', - correctionMessage: 'Consider using methods like MediaQuery.sizeOf or ' - 'MediaQuery.viewInsetsOf.', + 'prefer_dedicated_media_query_methods', + 'Prefer using dedicated MediaQuery methods.', + correctionMessage: + 'Consider using MediaQuery.sizeOf, widthOf, heightOf, etc. ' + 'instead of accessing properties on generic methods.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addMethodInvocation((node) { - final method = node.methodName.name; - final target = node.target?.toString(); - if (target == 'MediaQuery' && (method == 'of' || method == 'maybeOf')) { - reporter.atNode(node, _code); + final visitor = _Visitor(this, context); + registry.addMethodInvocation(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitMethodInvocation(MethodInvocation node) { + final method = node.methodName.name; + final target = node.target?.toString(); + + // Ignore if target is not MediaQuery + if (target != 'MediaQuery') { + return; + } + + // Check for MediaQuery.of / maybeOf + if (method == 'of' || method == 'maybeOf') { + rule.reportAtNode(node); + return; + } + + // Check for MediaQuery.sizeOf(context).width / height + if (method == 'sizeOf') { + // Check if the parent node is a property access (.width or .height) + final parent = node.parent; + if (parent is PropertyAccess) { + final propertyName = parent.propertyName.name; + if (propertyName == 'width' || propertyName == 'height') { + // Report on the entire expression including .width or .height + rule.reportAtNode(parent); + } } - }); + } } } diff --git a/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart b/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart index 2382906..7b4d547 100644 --- a/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart +++ b/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart @@ -1,7 +1,10 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; import 'package:analyzer/source/line_info.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; /// A `prefer_space_between_elements` rule that enforces /// spacing conventions within class definitions by requiring @@ -41,72 +44,89 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// } /// } /// ``` -class PreferSpaceBetweenElements extends DartLintRule { +class PreferSpaceBetweenElements extends AnalysisRule { /// Creates a new instance of [PreferSpaceBetweenElements]. - const PreferSpaceBetweenElements() : super(code: _code); + PreferSpaceBetweenElements() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'prefer_space_between_elements', - problemMessage: - 'Ensure there is a blank line between constructor and fields, ' + 'prefer_space_between_elements', + 'Ensure there is a blank line between constructor and fields, ' 'and between constructor and build method.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addClassDeclaration((node) { - final lineInfo = resolver.lineInfo; - final members = node.members; - for (var i = 0; i < members.length - 1; i++) { - final currentMember = members[i]; - final nextMember = members[i + 1]; + final visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; - // No blank line between constructor and build method. - if (currentMember is ConstructorDeclaration && - nextMember is MethodDeclaration && - nextMember.name.lexeme == 'build') { - if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { - reporter.atNode(nextMember, _code); - } + @override + void visitClassDeclaration(ClassDeclaration node) { + final lineInfo = node.thisOrAncestorOfType()?.lineInfo; + if (lineInfo == null) { + return; + } + final members = node.members; + for (var i = 0; i < members.length - 1; i++) { + final currentMember = members[i]; + final nextMember = members[i + 1]; + + // No blank line between constructor and build method. + if (currentMember is ConstructorDeclaration && + nextMember is MethodDeclaration && + nextMember.name.lexeme == 'build') { + if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { + rule.reportAtNode(nextMember); } + } - // No blank line between fields and constructor. - if (currentMember is FieldDeclaration && - nextMember is ConstructorDeclaration) { - if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { - reporter.atNode(nextMember, _code); - } + // No blank line between fields and constructor. + if (currentMember is FieldDeclaration && + nextMember is ConstructorDeclaration) { + if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { + rule.reportAtNode(nextMember); } + } - // No blank line between constructor and fields. - if (currentMember is ConstructorDeclaration && - nextMember is FieldDeclaration) { - if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { - reporter.atNode(nextMember, _code); - } + // No blank line between constructor and fields. + if (currentMember is ConstructorDeclaration && + nextMember is FieldDeclaration) { + if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { + rule.reportAtNode(nextMember); } + } - // No blank line between constructor and fields. - if (currentMember is FieldDeclaration && - nextMember is MethodDeclaration && - nextMember.name.lexeme == 'build') { - if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { - reporter.atNode(nextMember, _code); - } + // No blank line between fields and build method. + if (currentMember is FieldDeclaration && + nextMember is MethodDeclaration && + nextMember.name.lexeme == 'build') { + if (!_hasBlankLineBetween(currentMember, nextMember, lineInfo)) { + rule.reportAtNode(nextMember); } } - }); + } } /// Returns `true` if there is a blank line between [first] and [second]. bool _hasBlankLineBetween(AstNode first, AstNode second, LineInfo lineInfo) { final firstEndLine = lineInfo.getLocation(first.endToken.end).lineNumber; - final secondStartLine = - lineInfo.getLocation(second.beginToken.offset).lineNumber; + final secondStartLine = lineInfo + .getLocation(second.beginToken.offset) + .lineNumber; return (secondStartLine - firstEndLine) > 1; } } diff --git a/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart b/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart index 902326d..c13823c 100644 --- a/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart +++ b/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart @@ -1,7 +1,10 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; /// A `prefer_to_include_sliver_in_name` rule that ensures widgets returning /// a Sliver-type widget include "Sliver" in their class names. @@ -36,68 +39,80 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; /// } /// } /// ``` -class PreferToIncludeSliverInName extends DartLintRule { +class PreferToIncludeSliverInName extends AnalysisRule { /// Creates a new instance of [PreferToIncludeSliverInName]. - const PreferToIncludeSliverInName() : super(code: _code); + PreferToIncludeSliverInName() + : super(name: _code.name, description: _code.problemMessage); static const _code = LintCode( - name: 'prefer_to_include_sliver_in_name', - problemMessage: 'Widgets returning Sliver should include "Sliver" ' + 'prefer_to_include_sliver_in_name', + 'Widgets returning Sliver should include "Sliver" ' 'in the class name or named constructor.', - correctionMessage: - 'Consider adding "Sliver" to the class name or a named constructor.', ); @override - void run( - CustomLintResolver resolver, - ErrorReporter reporter, - CustomLintContext context, + DiagnosticCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addClassDeclaration((node) { - final methodBody = node.members - .whereType() - .firstWhereOrNull((method) => method.name.lexeme == 'build') - ?.body; - - if (methodBody is! BlockFunctionBody) { - return; - } - - final returnStatements = - methodBody.block.statements.whereType(); - final returnsSliverWidget = returnStatements.any( - (returnStatement) { - final returnType = returnStatement.expression?.staticType; - final typeName = returnType?.getDisplayString(); - return typeName?.startsWith('Sliver') ?? false; - }, - ); - - if (!returnsSliverWidget) { - return; - } - - final className = node.name.lexeme; - - if (className.contains('Sliver')) { - return; - } - - final constructorNames = node.members - .whereType() - .map((constructor) => constructor.name?.lexeme) - .nonNulls; - - final hasSliverInConstructor = constructorNames.any( - (constructorName) => constructorName.toLowerCase().contains('sliver'), - ); - - if (hasSliverInConstructor) { - return; - } - - reporter.atNode(node, _code); - }); + final visitor = _Visitor(this, context); + registry.addClassDeclaration(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + _Visitor(this.rule, this.context); + + final AnalysisRule rule; + final RuleContext context; + + @override + void visitClassDeclaration(ClassDeclaration node) { + final methodBody = node.members + .whereType() + .firstWhereOrNull((method) => method.name.lexeme == 'build') + ?.body; + + if (methodBody is! BlockFunctionBody) { + return; + } + + final returnStatements = methodBody.block.statements + .whereType(); + final returnsSliverWidget = returnStatements.any( + (returnStatement) { + final returnType = returnStatement.expression?.staticType; + final typeName = returnType?.getDisplayString(); + return typeName?.startsWith('Sliver') ?? false; + }, + ); + + if (!returnsSliverWidget) { + return; + } + + final className = node.name.lexeme; + + if (className.contains('Sliver')) { + return; + } + + final constructorNames = node.members + .whereType() + .map((constructor) => constructor.name?.lexeme) + .nonNulls; + + final hasSliverInConstructor = constructorNames.any( + (constructorName) => constructorName.toLowerCase().contains('sliver'), + ); + + if (hasSliverInConstructor) { + return; + } + + rule.reportAtNode(node); } } diff --git a/packages/altive_lints/lib/src/utils/files_utils.dart b/packages/altive_lints/lib/src/utils/files_utils.dart index b0f82cf..72996f8 100644 --- a/packages/altive_lints/lib/src/utils/files_utils.dart +++ b/packages/altive_lints/lib/src/utils/files_utils.dart @@ -1,7 +1,25 @@ -import 'package:analyzer/source/source.dart'; +import 'package:analyzer/file_system/file_system.dart'; -/// Whether the given [source] is a test file. -bool isTestFile(Source source) { - return source.uri.pathSegments.contains('test') || - source.shortName.endsWith('_test.dart'); +/// Whether the given [file] is in a test folder or a test file. +/// +/// /home/test/lib/test.dart +bool isTestFile(File file) { + final pathSegments = file.toUri().pathSegments; + final isInTestDir = pathSegments.contains('test'); + final isInLintsDir = pathSegments.contains('lints'); + final endsWithTest = file.shortName.endsWith('_test.dart'); + final isInLibDir = pathSegments.contains('lib'); + final isTestDart = file.shortName == 'test.dart'; + + // for lint rule test files + if (isInTestDir && isInLintsDir && endsWithTest) { + return false; + } + + // for reflectiveTest (/home/test/lib/test.dart) + if (isInLibDir && isTestDart) { + return false; + } + + return isInTestDir || endsWithTest; } diff --git a/packages/altive_lints/lib/src/utils/package_utils.dart b/packages/altive_lints/lib/src/utils/package_utils.dart new file mode 100644 index 0000000..f221b13 --- /dev/null +++ b/packages/altive_lints/lib/src/utils/package_utils.dart @@ -0,0 +1,39 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; + +/// Retrieves the package name from the pubspec.yaml file. +extension ResolvedCorrectionProducerExtension on ResolvedCorrectionProducer { + /// Retrieves the package name. + String? get packageName { + // Access the ResolvedUnitResult which contains the session and unit. + final result = unitResult; + + // [Fast] Try to get the URI from the declared fragment (New Analyzer API) + final uri = result.unit.declaredFragment?.source.uri; + + if (uri != null && uri.isScheme('package')) { + return uri.pathSegments.first; + } + + // [Robust] Try to parse pubspec.yaml (for test/bin/example folders) + try { + final contextRoot = result.session.analysisContext.contextRoot; + + // Look for pubspec.yaml in the context root + final pubspecFile = contextRoot.root.getChildAssumingFile('pubspec.yaml'); + + if (pubspecFile.exists) { + final content = pubspecFile.readAsStringSync(); + // Extract the name using Regex to avoid heavy YAML parsing + final match = RegExp( + r'^name:\s+([a-zA-Z0-9_]+)', + multiLine: true, + ).firstMatch(content); + return match?.group(1); + } + } on Exception catch (_) { + // Ignore errors (e.g., file read errors) + } + + return null; + } +} diff --git a/packages/altive_lints/lib/src/utils/types_utils.dart b/packages/altive_lints/lib/src/utils/types_utils.dart index c97d0ac..e3c1c79 100644 --- a/packages/altive_lints/lib/src/utils/types_utils.dart +++ b/packages/altive_lints/lib/src/utils/types_utils.dart @@ -82,7 +82,7 @@ bool _isWidget(DartType? type) => type?.getDisplayString() == 'Widget'; bool _isSubclassOfWidget(DartType? type) => type is InterfaceType && type.allSupertypes.any(_isWidget); -bool _isWidgetState(DartType? type) => type?.element3?.displayName == 'State'; +bool _isWidgetState(DartType? type) => type?.element?.displayName == 'State'; bool _isSubclassOfWidgetState(DartType? type) => type is InterfaceType && type.allSupertypes.any(_isWidgetState); diff --git a/packages/altive_lints/pubspec.yaml b/packages/altive_lints/pubspec.yaml index 5510b68..fecba99 100644 --- a/packages/altive_lints/pubspec.yaml +++ b/packages/altive_lints/pubspec.yaml @@ -11,15 +11,18 @@ topics: - analysis - code-style -environment: - sdk: ^3.5.0 - resolution: workspace +environment: + sdk: ^3.10.0 + dependencies: - analyzer: ">=7.0.0 <10.0.0" + analysis_server_plugin: ^0.3.0 + analyzer: ">=8.0.0 <10.0.0" + analyzer_plugin: ^0.13.11 collection: ^1.19.1 - custom_lint_builder: ^0.8.0 dev_dependencies: + analyzer_testing: ^0.1.7 test: ^1.26.2 + test_reflective_loader: ^0.4.0 diff --git a/packages/altive_lints/test/src/lints/avoid_consecutive_sliver_to_box_adapter_test.dart b/packages/altive_lints/test/src/lints/avoid_consecutive_sliver_to_box_adapter_test.dart new file mode 100644 index 0000000..630e329 --- /dev/null +++ b/packages/altive_lints/test/src/lints/avoid_consecutive_sliver_to_box_adapter_test.dart @@ -0,0 +1,100 @@ +import 'package:altive_lints/src/lints/avoid_consecutive_sliver_to_box_adapter.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidConsecutiveSliverToBoxAdapterTest); + }); +} + +@reflectiveTest +class AvoidConsecutiveSliverToBoxAdapterTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidConsecutiveSliverToBoxAdapter(); + super.setUp(); + } + + Future test_consecutive_sliver_to_box_adapter() async { + await assertDiagnostics( + ''' +void f() { + CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: Text('Item 1')), + SliverToBoxAdapter(child: Text('Item 2')), + ], + ); +} +$widgetClasses +''', + [lint(44, 113)], + ); + } + + Future test_no_consecutive_sliver_to_box_adapter() async { + await assertNoDiagnostics(''' +void f() { + CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: Text('Item 1')), + SliverList.list( + children: [ + Text('Item 2'), + ], + ), + ], + ); +} +$widgetClasses +'''); + } + + Future test_single_sliver_to_box_adapter() async { + await assertNoDiagnostics(''' +void f() { + CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: Text('Item 1')), + ], + ); +} +$widgetClasses +'''); + } + + Future test_empty_list() async { + await assertNoDiagnostics(''' +void f() { + CustomScrollView( + slivers: [], + ); +} +$widgetClasses +'''); + } +} + +const widgetClasses = ''' +class Widget { + const Widget(); +} + +class Text extends Widget { + const Text(String text); +} + +class SliverToBoxAdapter extends Widget { + const SliverToBoxAdapter({Widget? child}); +} + +class SliverList extends Widget { + const SliverList(); + static SliverList list({List? children}) => SliverList(); +} + +class CustomScrollView extends Widget { + const CustomScrollView({List? slivers}); +} +'''; diff --git a/packages/altive_lints/test/src/lints/avoid_hardcoded_japanese_test.dart b/packages/altive_lints/test/src/lints/avoid_hardcoded_japanese_test.dart new file mode 100644 index 0000000..95c44eb --- /dev/null +++ b/packages/altive_lints/test/src/lints/avoid_hardcoded_japanese_test.dart @@ -0,0 +1,91 @@ +// Because it's a test +// ignore_for_file: altive_lints/avoid_hardcoded_japanese +import 'package:altive_lints/src/lints/avoid_hardcoded_japanese.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidHardcodedJapaneseTest); + }); +} + +@reflectiveTest +class AvoidHardcodedJapaneseTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidHardcodedJapanese(); + super.setUp(); + } + + Future test_simple_string_literal_with_japanese() async { + await assertDiagnostics( + ''' + void f() { + final message = 'こんにちは'; + } + ''', + [lint(33, 7)], + ); + } + + Future test_string_interpolation_with_japanese() async { + await assertDiagnostics( + ''' + void f() { + print('エラーが発生しました'); + } + ''', + [lint(23, 12)], + ); + } + + Future test_simple_string_literal_without_japanese() async { + await assertNoDiagnostics(''' +void f() { + final message = 'Hello'; +} +'''); + } + + Future test_string_interpolation_without_japanese() async { + await assertNoDiagnostics(''' +void f() { + print('Error occurred'); +} +'''); + } + + Future test_hiragana() async { + await assertDiagnostics( + ''' + void f() { + final message = 'あいうえお'; + } + ''', + [lint(33, 7)], + ); + } + + Future test_katakana() async { + await assertDiagnostics( + ''' + void f() { + final message = 'アイウエオ'; + } + ''', + [lint(33, 7)], + ); + } + + Future test_kanji() async { + await assertDiagnostics( + ''' + void f() { + final message = '日本語'; + } + ''', + [lint(33, 5)], + ); + } +} diff --git a/packages/altive_lints/test/src/lints/avoid_shrink_wrap_in_list_view_test.dart b/packages/altive_lints/test/src/lints/avoid_shrink_wrap_in_list_view_test.dart new file mode 100644 index 0000000..8d8785d --- /dev/null +++ b/packages/altive_lints/test/src/lints/avoid_shrink_wrap_in_list_view_test.dart @@ -0,0 +1,121 @@ +import 'package:altive_lints/src/lints/avoid_shrink_wrap_in_list_view.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidShrinkWrapInListViewTest); + }); +} + +@reflectiveTest +class AvoidShrinkWrapInListViewTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidShrinkWrapInListView(); + super.setUp(); + } + + Future test_listView_shrinkWrap_inside_listView() async { + await assertDiagnostics( + ''' +void f() { + ListView( + children: [ + ListView( + shrinkWrap: true, + children: [], + ), + ], + ); +} +$widgetClasses +''', + [lint(45, 65)], + ); + } + + Future test_listView_shrinkWrap_inside_column() async { + await assertDiagnostics( + ''' +void f() { + Column( + children: [ + ListView( + shrinkWrap: true, + children: [], + ), + ], + ); +} +$widgetClasses +''', + [lint(43, 65)], + ); + } + + Future test_listView_shrinkWrap_inside_row() async { + await assertDiagnostics( + ''' +void f() { + Row( + children: [ + ListView( + shrinkWrap: true, + children: [], + ), + ], + ); +} +$widgetClasses +''', + [lint(40, 65)], + ); + } + + Future test_listView_no_shrinkWrap() async { + await assertNoDiagnostics( + ''' +void f() { + ListView( + children: [ + ListView( + children: [], + ), + ], + ); +} +$widgetClasses +''', + ); + } + + Future test_listView_shrinkWrap_no_parent_list() async { + await assertNoDiagnostics( + ''' +void f() { + ListView( + shrinkWrap: true, + children: [], + ); +} +$widgetClasses +''', + ); + } +} + +const widgetClasses = ''' +class Widget { + const Widget(); +} +class Column extends Widget { + const Column({List children = const []}); +} +class Row extends Widget { + const Row({List children = const []}); +} +class ListView extends Widget { + const ListView({bool shrinkWrap = false, List children = const []}); +} +'''; diff --git a/packages/altive_lints/test/src/lints/avoid_single_child_test.dart b/packages/altive_lints/test/src/lints/avoid_single_child_test.dart new file mode 100644 index 0000000..392fc9c --- /dev/null +++ b/packages/altive_lints/test/src/lints/avoid_single_child_test.dart @@ -0,0 +1,543 @@ +import 'package:altive_lints/src/lints/avoid_single_child.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidSingleChildTest); + }); +} + +@reflectiveTest +class AvoidSingleChildTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidSingleChild(); + super.setUp(); + } + + Future test_column_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Column( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 50)], + ); + } + + Future test_row_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Row( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 47)], + ); + } + + Future test_flex_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Flex( + direction: Axis.horizontal, + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 80)], + ); + } + + Future test_wrap_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Wrap( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 48)], + ); + } + + Future test_stack_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Stack( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 49)], + ); + } + + Future test_listView_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + ListView( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 52)], + ); + } + + Future test_sliverList_single_sliver() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + SliverList( + slivers: [ + Widget(), + ], + ); +} +''', + [lint(1252, 53)], + ); + } + + Future test_sliverList_list_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + SliverList.list( + children: [ + Widget(), + ], + ); +} +''', + [lint(1252, 59)], + ); + } + + Future test_sliverMainAxisGroup_single_sliver() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter(child: Widget()), + ], + ); +} +''', + [lint(1252, 89)], + ); + } + + Future test_sliverCrossAxisGroup_single_sliver() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + SliverCrossAxisGroup( + slivers: [ + SliverToBoxAdapter(child: Widget()), + ], + ); +} +''', + [lint(1252, 90)], + ); + } + + Future test_column_multiple_children() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + Column( + children: [ + Widget(), + Widget(), + ], + ); +} +''', + ); + } + + Future test_column_if_single_child() async { + await assertDiagnostics( + ''' +$widgetClasses +void f() { + Column( + children: [ + if (true) Widget(), + ], + ); +} +''', + [lint(1252, 60)], + ); + } + + Future test_column_if_spread_multiple_children() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + Column( + children: [ + if (true) ...[Widget(), Widget()], + ], + ); +} +''', + ); + } + + Future test_column_if_else_spread_multiple_children() async { + await assertNoDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) Widget() else ...[Widget(), Widget()], + ], + ); +} +''', + ); + } + + Future test_column_if_spread_single_else_spread_single() async { + await assertDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) ...[ + Container(), + ] else ...[ + Container(), + ], + ], + ); +} +''', + [lint(1274, 129)], + ); + } + + Future test_column_if_spread_single_followed_by_child() async { + await assertNoDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) ...[ + Container(), + ], + Container(), + ], + ); +} +''', + ); + } + + Future test_column_if_single_spread_empty() async { + await assertDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) ...[ + Container(), + ], + ], + ); +} +''', + [lint(1274, 90)], + ); + } + + Future test_column_if_spread_multiple_single() async { + await assertNoDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) ...[ + Container(), + Container(), + ], + ], + ); +} +''', + ); + } + + Future test_column_if_spread_empty_else_spread_empty() async { + await assertDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [if (condition) ...[] else ...[]], + ); +} +''', + [lint(1274, 60)], + ); + } + + Future test_column_if_spread_empty_else_spread_single() async { + await assertDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) + ...[] + else ...[ + Container(), + ] + ], + ); +} +''', + [lint(1274, 114)], + ); + } + + Future test_column_conditional_list_literals() async { + await assertNoDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: condition + ? [ + Text('Hello World'), + ] + : [ + Text('Hello'), + Text('World'), + ], + ); +} +''', + ); + } + + Future test_if_element_top_level() async { + await assertDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) + Text('Hello World') + else + Column( + children: [ + Text('Hello'), + Text('World'), + ], + ), + ], + ); +} +''', + [lint(1274, 210)], + ); + } + + Future test_listView_list_generate() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + ListView( + children: List.generate( + 10, + (index) => Text('Hello'), + ), + ); +} +''', + ); + } + + Future test_row_for_element() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + Row( + children: [ + for (var i = 0; i < 10; i++) Text('Hello'), + ], + ); +} +''', + ); + } + + Future test_sliverList_list_for_element() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + SliverList.list( + children: [ + for (final e in ['a', 'b', 'c']) Text('Hello'), + ], + ); +} +''', + ); + } + + Future test_sliverMainAxisGroup_multiple_slivers() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + SliverMainAxisGroup( + slivers: [ + SliverAppBar( + title: Text('Sliver App Bar'), + ), + SliverToBoxAdapter( + child: Text('Hello World'), + ), + ], + ); +} +''', + ); + } + + Future test_column_multiple_ifs() async { + await assertNoDiagnostics( + ''' +$widgetClasses +var condition = true; +void f() { + Column( + children: [ + if (condition) Text('Hello 1'), + if (condition) Text('Hello 2'), + ], + ); +} +''', + ); + } + + Future test_column_empty_children() async { + await assertNoDiagnostics( + ''' +$widgetClasses +void f() { + Column(children: []); +} +''', + ); + } +} + +const widgetClasses = ''' +class Widget { + const Widget(); +} +class Column extends Widget { + const Column({List children = const []}); +} +class Row extends Widget { + const Row({List children = const []}); +} +class Flex extends Widget { + const Flex({required Axis direction, List children = const []}); +} +class Wrap extends Widget { + const Wrap({List children = const []}); +} +class Stack extends Widget { + const Stack({List children = const []}); +} +class ListView extends Widget { + const ListView({List children = const []}); +} +class SliverList extends Widget { + const SliverList({List slivers = const []}); + const SliverList.list({List children = const []}); +} +class SliverMainAxisGroup extends Widget { + const SliverMainAxisGroup({List slivers = const []}); +} +class SliverCrossAxisGroup extends Widget { + const SliverCrossAxisGroup({List slivers = const []}); +} +class SliverToBoxAdapter extends Widget { + const SliverToBoxAdapter({Widget? child}); +} +class SliverAppBar extends Widget { + const SliverAppBar({Widget? title}); +} +class Text extends Widget { + const Text(String data); +} +class Container extends Widget { + const Container(); +} +enum Axis { horizontal, vertical } +'''; diff --git a/packages/altive_lints/test/src/lints/prefer_clock_now_test.dart b/packages/altive_lints/test/src/lints/prefer_clock_now_test.dart new file mode 100644 index 0000000..3b38d6c --- /dev/null +++ b/packages/altive_lints/test/src/lints/prefer_clock_now_test.dart @@ -0,0 +1,41 @@ +import 'package:altive_lints/src/lints/prefer_clock_now.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferClockNowTest); + }); +} + +@reflectiveTest +class PreferClockNowTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferClockNow(); + super.setUp(); + } + + Future test_has_datetime_now() async { + await assertDiagnostics( + ''' +void f() { + DateTime.now(); +} +''', + [lint(13, 14)], + ); + } + + Future test_no_datetime_now() async { + await assertNoDiagnostics(''' +class Clock { + int now() => 0; +} +final clock = Clock(); +void f() { + clock.now(); +} +'''); + } +} diff --git a/packages/altive_lints/test/src/lints/prefer_dedicated_media_query_methods_test.dart b/packages/altive_lints/test/src/lints/prefer_dedicated_media_query_methods_test.dart new file mode 100644 index 0000000..10cd37c --- /dev/null +++ b/packages/altive_lints/test/src/lints/prefer_dedicated_media_query_methods_test.dart @@ -0,0 +1,122 @@ +import 'package:altive_lints/src/lints/prefer_dedicated_media_query_methods.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferDedicatedMediaQueryMethodsTest); + }); +} + +@reflectiveTest +class PreferDedicatedMediaQueryMethodsTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferDedicatedMediaQueryMethods(); + super.setUp(); + } + + Future test_mediaQuery_of() async { + await assertDiagnostics( + ''' +void f(BuildContext context) { + MediaQuery.of(context); +} +$mockClasses +''', + [lint(33, 22)], + ); + } + + Future test_mediaQuery_maybeOf() async { + await assertDiagnostics( + ''' +void f(BuildContext context) { + MediaQuery.maybeOf(context); +} +$mockClasses +''', + [lint(33, 27)], + ); + } + + Future test_mediaQuery_sizeOf() async { + await assertNoDiagnostics( + ''' +void f(BuildContext context) { + MediaQuery.sizeOf(context); +} +$mockClasses +''', + ); + } + + Future test_mediaQuery_viewInsetsOf() async { + await assertNoDiagnostics( + ''' +void f(BuildContext context) { + MediaQuery.viewInsetsOf(context); +} +$mockClasses +''', + ); + } + + Future test_mediaQuery_of_property_access() async { + await assertDiagnostics( + ''' +void f(BuildContext context) { + var size = MediaQuery.of(context).size; +} +$mockClasses +''', + [lint(44, 22)], + ); + } + + Future test_mediaQuery_sizeOf_width() async { + await assertDiagnostics( + ''' +void f(BuildContext context) { + var width = MediaQuery.sizeOf(context).width; +} +$mockClasses +''', + [lint(45, 32)], + ); + } + + Future test_mediaQuery_sizeOf_height() async { + await assertDiagnostics( + ''' +void f(BuildContext context) { + var height = MediaQuery.sizeOf(context).height; +} +$mockClasses +''', + [lint(46, 33)], + ); + } +} + +const mockClasses = ''' +class BuildContext {} + +class MediaQuery { + static MediaQueryData of(BuildContext context) => MediaQueryData(); + static MediaQueryData? maybeOf(BuildContext context) => MediaQueryData(); + static Size sizeOf(BuildContext context) => Size(); + static EdgeInsets viewInsetsOf(BuildContext context) => EdgeInsets(); +} + +class MediaQueryData { + Size get size => Size(); + EdgeInsets get padding => EdgeInsets(); +} + +class Size { + double get width => 0; + double get height => 0; +} +class EdgeInsets {} +'''; diff --git a/packages/altive_lints/test/src/lints/prefer_space_between_elements_test.dart b/packages/altive_lints/test/src/lints/prefer_space_between_elements_test.dart new file mode 100644 index 0000000..0123b6c --- /dev/null +++ b/packages/altive_lints/test/src/lints/prefer_space_between_elements_test.dart @@ -0,0 +1,90 @@ +import 'package:altive_lints/src/lints/prefer_space_between_elements.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferSpaceBetweenElementsTest); + }); +} + +@reflectiveTest +class PreferSpaceBetweenElementsTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferSpaceBetweenElements(); + super.setUp(); + } + + Future test_field_constructor_no_space() async { + await assertDiagnostics( + ''' +class MyWidget { + final String title; + MyWidget(this.title); +} +''', + [lint(41, 21)], + ); + } + + Future test_constructor_build_no_space() async { + await assertDiagnostics( + ''' +class MyWidget { + MyWidget(); + void build() {} +} +''', + [lint(33, 15)], + ); + } + + Future test_field_constructor_with_space() async { + await assertNoDiagnostics( + ''' +class MyWidget { + final String title; + + MyWidget(this.title); +} +''', + ); + } + + Future test_constructor_build_with_space() async { + await assertNoDiagnostics( + ''' +class MyWidget { + MyWidget(); + + void build() {} +} +''', + ); + } + + Future test_constructor_field_no_space() async { + await assertDiagnostics( + ''' +class MyWidget { + MyWidget(); + final String title = ''; +} +''', + [lint(33, 24)], + ); + } + + Future test_field_build_no_space() async { + await assertDiagnostics( + ''' +class MyWidget { + final String title = ''; + void build() {} +} +''', + [lint(46, 15)], + ); + } +} diff --git a/packages/altive_lints/test/src/lints/prefer_to_include_sliver_in_name_test.dart b/packages/altive_lints/test/src/lints/prefer_to_include_sliver_in_name_test.dart new file mode 100644 index 0000000..119b8e9 --- /dev/null +++ b/packages/altive_lints/test/src/lints/prefer_to_include_sliver_in_name_test.dart @@ -0,0 +1,93 @@ +import 'package:altive_lints/src/lints/prefer_to_include_sliver_in_name.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferToIncludeSliverInNameTest); + }); +} + +@reflectiveTest +class PreferToIncludeSliverInNameTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferToIncludeSliverInName(); + super.setUp(); + } + + Future test_class_without_sliver_in_name_returns_sliver() async { + await assertDiagnostics( + ''' +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemCount: 10, + itemBuilder: (context, index) => Text('Item'), + ); + } +} +$_widgetClasses +''', + [lint(0, 209)], + ); + } + + Future test_class_with_sliver_in_name_returns_sliver() async { + await assertNoDiagnostics(''' +class MySliverWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemCount: 10, + itemBuilder: (context, index) => Text('Item'), + ); + } +} +$_widgetClasses +'''); + } + + Future test_class_without_sliver_returns_non_sliver() async { + await assertNoDiagnostics(''' +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: 10, + itemBuilder: (context, index) => Text('Item'), + ); + } +} +$_widgetClasses +'''); + } +} + +const _widgetClasses = ''' +class BuildContext {} + +class Widget { + const Widget(); +} + +class StatelessWidget extends Widget { + const StatelessWidget(); + Widget build(BuildContext context) => const Widget(); +} + +class SliverList extends Widget { + const SliverList(); + const SliverList.builder({int? itemCount, Widget Function(BuildContext, int)? itemBuilder}); +} + +class ListView extends Widget { + const ListView(); + const ListView.builder({int? itemCount, Widget Function(BuildContext, int)? itemBuilder}); +} + +class Text extends Widget { + const Text(String data); +} +'''; diff --git a/packages/altive_lints/test/src/utils/files_utils_test.dart b/packages/altive_lints/test/src/utils/files_utils_test.dart index 2341fba..30f821f 100644 --- a/packages/altive_lints/test/src/utils/files_utils_test.dart +++ b/packages/altive_lints/test/src/utils/files_utils_test.dart @@ -1,6 +1,5 @@ import 'package:altive_lints/src/utils/files_utils.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; -import 'package:analyzer/source/file_source.dart'; import 'package:test/test.dart'; void main() { @@ -10,8 +9,7 @@ void main() { const filePath = '/project/lib/src/implementation.dart'; final file = resourceProvider.getFile(filePath); - final source = FileSource(file); - expect(isTestFile(source), isFalse); + expect(isTestFile(file), isFalse); }); test('True if it is a test file', () { @@ -19,8 +17,7 @@ void main() { const filePath = '/project/lib/src/implementation_test.dart'; final file = resourceProvider.getFile(filePath); - final source = FileSource(file); - expect(isTestFile(source), isTrue); + expect(isTestFile(file), isTrue); }); test('True if it is a test directory', () { @@ -28,8 +25,47 @@ void main() { const filePath = '/project/test/src/util.dart'; final file = resourceProvider.getFile(filePath); - final source = FileSource(file); - expect(isTestFile(source), isTrue); + expect(isTestFile(file), isTrue); }); + + test('False if path contains test but not in test directory', () { + final resourceProvider = PhysicalResourceProvider.INSTANCE; + + const filePath = '/project/testfile.dart'; + final file = resourceProvider.getFile(filePath); + expect(isTestFile(file), isFalse); + }); + + test( + 'False if the path or directory contains the string "test" ' + 'but it is not an exact match', + () { + final resourceProvider = PhysicalResourceProvider.INSTANCE; + + const filePath = '/project/testable/testable_file.dart'; + final file = resourceProvider.getFile(filePath); + expect(isTestFile(file), isFalse); + }, + ); + + test('False if it is in lib directory and file name is test.dart', () { + final resourceProvider = PhysicalResourceProvider.INSTANCE; + + const filePath = '/home/test/lib/test.dart'; + final file = resourceProvider.getFile(filePath); + expect(isTestFile(file), isFalse); + }); + + test( + 'False if it is in test dir and lints dir and ends with _test.dart', + () { + final resourceProvider = PhysicalResourceProvider.INSTANCE; + + const filePath = + '/project/test/src/lints/avoid_hardcoded_japanese_test.dart'; + final file = resourceProvider.getFile(filePath); + expect(isTestFile(file), isFalse); + }, + ); }); } diff --git a/packages/diffscrape/pubspec.yaml b/packages/diffscrape/pubspec.yaml index 5fccdcc..6cd1a80 100644 --- a/packages/diffscrape/pubspec.yaml +++ b/packages/diffscrape/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none repository: https://github.com/altive/altive_lints environment: - sdk: ^3.5.0 + sdk: ^3.10.0 resolution: workspace diff --git a/pubspec.yaml b/pubspec.yaml index bacd970..4f8f4de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: altive_lints_workspace publish_to: none environment: - sdk: ^3.5.0 + sdk: ^3.10.0 workspace: - packages/altive_lints @@ -11,31 +11,9 @@ workspace: dev_dependencies: altive_lints: path: packages/altive_lints - melos: ^7.0.0 + melos: ^7.3.0 melos: - command: version: workspaceChangelog: false - - scripts: - analyze: - exec: dart analyze --fatal-infos lib - description: Run melos analyze. - - custom_lint: - exec: dart run custom_lint - description: Run custom_lint. - packageFilters: - dependsOn: "custom_lint" - - # Issue on file exclusion feature: https://github.com/dart-lang/dart_style/issues/864 - # NOTE: Using the `exec:` format causes processing to stop - format:ci: - run: | - melos exec -- \ - dart format --set-exit-if-changed lib/ - description: Run dart format for CI. - packageFilters: - dirExists: lib From bbda88733e5f4509adab9164a6f8affcbeb17125 Mon Sep 17 00:00:00 2001 From: Ryunosuke Muramatsu Date: Fri, 5 Dec 2025 00:43:52 +0900 Subject: [PATCH 2/3] build: bump altive_lints to 2.0.0-dev.1 --- packages/altive_lints/README.md | 2 +- packages/altive_lints/pubspec.yaml | 6 +++--- packages/diffscrape/pubspec.yaml | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/altive_lints/README.md b/packages/altive_lints/README.md index 346511e..26d46b7 100644 --- a/packages/altive_lints/README.md +++ b/packages/altive_lints/README.md @@ -59,7 +59,7 @@ linter: - public_member_api_docs: false plugins: - altive_lints: ^2.0.0 + altive_lints: ^2.0.0-dev.1 diagnostics: # Explicitly disable one analysis rule. avoid_consecutive_sliver_to_box_adapter: false diff --git a/packages/altive_lints/pubspec.yaml b/packages/altive_lints/pubspec.yaml index fecba99..5d3ddd7 100644 --- a/packages/altive_lints/pubspec.yaml +++ b/packages/altive_lints/pubspec.yaml @@ -2,14 +2,14 @@ name: altive_lints description: >- Provides `all_lint_rules.yaml` that activates all rules and `altive_lints.yaml` with Altive recommended rule selection. -version: 1.25.0 +version: 2.0.0-dev.1 homepage: https://altive.dev repository: https://github.com/altive/altive_lints issue_tracker: https://github.com/altive/altive_lints/issues topics: - - lints - analysis - - code-style + - analyzer + - lints resolution: workspace diff --git a/packages/diffscrape/pubspec.yaml b/packages/diffscrape/pubspec.yaml index 6cd1a80..0f26462 100644 --- a/packages/diffscrape/pubspec.yaml +++ b/packages/diffscrape/pubspec.yaml @@ -15,5 +15,6 @@ dependencies: http: ^1.4.0 dev_dependencies: - altive_lints: ^1.25.0 + altive_lints: + path: ../altive_lints test: ^1.26.2 From b4cb3d69b65b2ab8a904117fc436af25276032ab Mon Sep 17 00:00:00 2001 From: Ryunosuke Muramatsu Date: Fri, 5 Dec 2025 08:59:40 +0900 Subject: [PATCH 3/3] chore: fix comments --- .../avoid_consecutive_sliver_to_box_adapter.dart | 4 +++- .../lib/src/lints/avoid_hardcoded_color.dart | 16 +++------------- .../lib/src/lints/avoid_hardcoded_japanese.dart | 4 +++- .../lints/avoid_shrink_wrap_in_list_view.dart | 4 +++- .../lib/src/lints/avoid_single_child.dart | 4 +++- .../lib/src/lints/prefer_clock_now.dart | 5 +++-- .../prefer_dedicated_media_query_methods.dart | 4 +++- .../src/lints/prefer_space_between_elements.dart | 4 +++- .../lints/prefer_to_include_sliver_in_name.dart | 4 +++- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart index 1aa54b3..2e63b87 100644 --- a/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart +++ b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +/// {@template altive_lints.AvoidConsecutiveSliverToBoxAdapter} /// A `avoid_consecutive_sliver_to_box_adapter` rule that /// identifies and discourages the use of consecutive /// `SliverToBoxAdapter` widgets within a list. @@ -43,8 +44,9 @@ import 'package:analyzer/error/error.dart'; /// ], /// ); /// ``` +/// {@endtemplate} class AvoidConsecutiveSliverToBoxAdapter extends AnalysisRule { - /// Creates a new instance of [AvoidConsecutiveSliverToBoxAdapter]. + /// {@macro altive_lints.AvoidConsecutiveSliverToBoxAdapter} AvoidConsecutiveSliverToBoxAdapter() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart index 912a8cb..2f6ff28 100644 --- a/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart @@ -75,8 +75,6 @@ class _Visitor extends SimpleAstVisitor { return; } - // constructorName.staticElement が存在しない場合があるため、 - // 生成される型 (staticType) からクラス要素を取得する方法に変更 final type = node.staticType; if (_isColorClass(type?.element)) { rule.reportAtNode(node); @@ -89,19 +87,16 @@ class _Visitor extends SimpleAstVisitor { return; } - // methodName.staticElement -> methodName.element (互換性のため) final element = node.methodName.element; - // コンストラクタ呼び出しの場合のチェック if (element is ConstructorElement) { - // enclosingElement3 -> enclosingElement (互換性のため) if (_isColorClass(element.enclosingElement)) { rule.reportAtNode(node); } return; } - // 静的メソッド (Color.fromARGBなど) の場合 + // e.g. Color.fromARGB if (element is MethodElement && element.isStatic) { if (_isColorClass(element.enclosingElement)) { rule.reportAtNode(node); @@ -118,7 +113,6 @@ class _Visitor extends SimpleAstVisitor { final element = node.element; if (element is PropertyAccessorElement || element is FieldElement) { - // enclosingElement3 -> enclosingElement final parentClass = element?.enclosingElement; if (parentClass is ClassElement && parentClass.name == 'Colors') { // Colors.transparent is allowed @@ -160,26 +154,22 @@ class _Visitor extends SimpleAstVisitor { bool _isInsideColorScheme(AstNode node) { var parent = node.parent; while (parent != null) { - // 1. Check if we are inside a ColorScheme(...) constructor + // Check if we are inside a ColorScheme(...) constructor if (parent is InstanceCreationExpression) { final type = parent.staticType; - // type.element は古いバージョンでも存在します if (type != null && type.element?.name == 'ColorScheme') { return true; } } - // 2. Check if we are inside a method call on a ColorScheme object + // Check if we are inside a method call on a ColorScheme object if (parent is MethodInvocation) { final targetType = parent.target?.staticType; if (targetType != null && targetType.element?.name == 'ColorScheme') { return true; } - // Fallback - // methodName.staticElement -> methodName.element final methodElement = parent.methodName.element; - // enclosingElement3 -> enclosingElement if (methodElement?.enclosingElement?.name == 'ColorScheme') { return true; } diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart index cb681b3..d2cd41d 100644 --- a/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_japanese.dart @@ -7,6 +7,7 @@ import 'package:analyzer/error/error.dart'; import '../utils/files_utils.dart'; +/// {@template altive_lints.AvoidHardcodedJapanese} /// An `avoid_hardcoded_japanese` rule which detects /// and reports hardcoded Japanese text strings within the code. /// @@ -29,8 +30,9 @@ import '../utils/files_utils.dart'; /// print(AppLocalizations.of(context).errorOccurred); /// ``` /// +/// {@endtemplate} class AvoidHardcodedJapanese extends AnalysisRule { - /// Creates a new instance of [AvoidHardcodedJapanese]. + /// {@macro altive_lints.AvoidHardcodedJapanese} AvoidHardcodedJapanese() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart b/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart index 0a6d10e..4ced07b 100644 --- a/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart +++ b/packages/altive_lints/lib/src/lints/avoid_shrink_wrap_in_list_view.dart @@ -8,6 +8,7 @@ import 'package:collection/collection.dart'; import '../utils/types_utils.dart'; +/// {@template altive_lints.AvoidShrinkWrapInListView} /// An `avoid_shrink_wrap_in_list_view` rule that discourages /// using `shrinkWrap` with `ListView`. /// @@ -44,8 +45,9 @@ import '../utils/types_utils.dart'; /// ], /// ); /// ``` +/// {@endtemplate} class AvoidShrinkWrapInListView extends AnalysisRule { - /// Creates a new instance of [AvoidShrinkWrapInListView]. + /// {@macro altive_lints.AvoidShrinkWrapInListView} AvoidShrinkWrapInListView() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/avoid_single_child.dart b/packages/altive_lints/lib/src/lints/avoid_single_child.dart index b165038..3335a79 100644 --- a/packages/altive_lints/lib/src/lints/avoid_single_child.dart +++ b/packages/altive_lints/lib/src/lints/avoid_single_child.dart @@ -6,6 +6,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; import 'package:collection/collection.dart'; +/// {@template altive_lints.AvoidSingleChild} /// An `avoid_single_child` rule that warns against using layout /// widgets intended for multiple children with only one child. /// @@ -36,8 +37,9 @@ import 'package:collection/collection.dart'; /// children: [YourWidget1(), YourWidget2()], /// ); /// ``` +/// {@endtemplate} class AvoidSingleChild extends AnalysisRule { - /// Creates a new instance of [AvoidSingleChild]. + /// {@macro altive_lints.AvoidSingleChild} AvoidSingleChild() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/prefer_clock_now.dart b/packages/altive_lints/lib/src/lints/prefer_clock_now.dart index 07f6dbe..b80f401 100644 --- a/packages/altive_lints/lib/src/lints/prefer_clock_now.dart +++ b/packages/altive_lints/lib/src/lints/prefer_clock_now.dart @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +/// {@template altive_lints.PreferClockNow} /// A `prefer_clock_now` rule that discourages the use of /// `DateTime.now()` due to its non-testability in unit tests. /// @@ -25,11 +26,11 @@ import 'package:analyzer/error/error.dart'; /// ```dart /// var now = clock.now(); // Using 'clock' package /// ``` +/// {@endtemplate} class PreferClockNow extends AnalysisRule { - /// + /// {@macro altive_lints.PreferClockNow} PreferClockNow() : super(name: _code.name, description: _code.problemMessage); - /// static const _code = LintCode( 'prefer_clock_now', 'Avoid using DateTime.now(). ' diff --git a/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart b/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart index e36ad8b..dc7bd7e 100644 --- a/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart +++ b/packages/altive_lints/lib/src/lints/prefer_dedicated_media_query_methods.dart @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +/// {@template altive_lints.PreferDedicatedMediaQueryMethods} /// A `prefer_dedicated_media_query_methods` rule that encourages /// the use of dedicated `MediaQuery` methods instead of /// the generic `MediaQuery.of` or `MediaQuery.maybeOf`. @@ -32,8 +33,9 @@ import 'package:analyzer/error/error.dart'; /// var size = MediaQuery.sizeOf(context); /// var padding = MediaQuery.viewInsetsOf(context); /// ``` +/// {@endtemplate} class PreferDedicatedMediaQueryMethods extends AnalysisRule { - /// Creates a new instance of [PreferDedicatedMediaQueryMethods]. + /// {@macro altive_lints.PreferDedicatedMediaQueryMethods} PreferDedicatedMediaQueryMethods() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart b/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart index 7b4d547..5035acc 100644 --- a/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart +++ b/packages/altive_lints/lib/src/lints/prefer_space_between_elements.dart @@ -6,6 +6,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; import 'package:analyzer/source/line_info.dart'; +/// {@template altive_lints.PreferSpaceBetweenElements} /// A `prefer_space_between_elements` rule that enforces /// spacing conventions within class definitions by requiring /// a blank line between the constructor and fields, @@ -44,8 +45,9 @@ import 'package:analyzer/source/line_info.dart'; /// } /// } /// ``` +/// {@endtemplate} class PreferSpaceBetweenElements extends AnalysisRule { - /// Creates a new instance of [PreferSpaceBetweenElements]. + /// {@macro altive_lints.PreferSpaceBetweenElements} PreferSpaceBetweenElements() : super(name: _code.name, description: _code.problemMessage); diff --git a/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart b/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart index c13823c..9b5b24a 100644 --- a/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart +++ b/packages/altive_lints/lib/src/lints/prefer_to_include_sliver_in_name.dart @@ -6,6 +6,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; import 'package:collection/collection.dart'; +/// {@template altive_lints.PreferToIncludeSliverInName} /// A `prefer_to_include_sliver_in_name` rule that ensures widgets returning /// a Sliver-type widget include "Sliver" in their class names. /// @@ -39,8 +40,9 @@ import 'package:collection/collection.dart'; /// } /// } /// ``` +/// {@endtemplate} class PreferToIncludeSliverInName extends AnalysisRule { - /// Creates a new instance of [PreferToIncludeSliverInName]. + /// {@macro altive_lints.PreferToIncludeSliverInName} PreferToIncludeSliverInName() : super(name: _code.name, description: _code.problemMessage);