diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25dfa148..fa591e87 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,3 +68,49 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} flags: ${{ matrix.package }} files: ./${{ matrix.package }}/coverage/lcov.txt + + build_lint_packages: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ matrix.package }} + + strategy: + matrix: + package: ["mobx_lint", "mobx_lint_flutter_test"] + version: ["stable"] + + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: ${{ matrix.channel }} + - name: Add pub cache bin to PATH + run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH + - name: Add pub cache to PATH + run: echo "PUB_CACHE="$HOME/.pub-cache"" >> $GITHUB_ENV + - name: Install dependencies + run: flutter pub get + - run: dart pub global activate custom_lint + + - name: Analyze + run: flutter analyze + + - name: Run custom_lint + run: custom_lint + # Workaround to https://github.com/invertase/dart_custom_lint/issues/77 + if: matrix.package == 'mobx_lint_flutter_test' + + - name: Run tests + if: matrix.package == 'mobx_lint_flutter_test' + # Workaround to https://github.com/dart-lang/sdk/issues/53530 + run: ../tool/coverage.sh + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v3 + if: matrix.package == 'mobx_lint_flutter_test' + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: mobx_lint + files: coverage/lcov.txt \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9551f10b..917a6672 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - package: [ "mobx", "mobx_codegen" ] + package: [ "mobx", "mobx_codegen", "mobx_lint" ] steps: - uses: actions/checkout@v3 diff --git a/codecov.yml b/codecov.yml index d0831e07..7c1ea1ce 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,6 +9,8 @@ coverage: flags: mobx_codegen flutter_mobx: flags: flutter_mobx + mobx_lint: + flags: mobx_lint flags: # filter the folder(s) you wish to measure by that flag @@ -22,3 +24,6 @@ flags: flutter_mobx: paths: - flutter_mobx/lib/ + mobx_lint: + paths: + - mobx_lint/lib/ \ No newline at end of file diff --git a/mobx_lint/CHANGELOG.md b/mobx_lint/CHANGELOG.md new file mode 100644 index 00000000..1ad71e67 --- /dev/null +++ b/mobx_lint/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- add wrap with observer assist by [@amondnet](https://github.com/amondnet) diff --git a/mobx_lint/LICENSE b/mobx_lint/LICENSE new file mode 100644 index 00000000..b26a0c15 --- /dev/null +++ b/mobx_lint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 MobX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mobx_lint/README.md b/mobx_lint/README.md new file mode 100644 index 00000000..bdd54304 --- /dev/null +++ b/mobx_lint/README.md @@ -0,0 +1,52 @@ +mobx_lint is a developer tool for users of mobx, designed to help stop common issues and simplify repetitive tasks. + +mobx_lint adds various warnings with quick fixes and refactoring options, such as: + +- Refactor wrap a widget with Observer + +## Installing mobx_lint + +mobx_lint is implemented using [custom_lint]. As such, it uses custom_lint's installation logic. +Long story short: + +- Add both mobx_lint and custom_lint to your `pubspec.yaml`: + ```yaml + dev_dependencies: + custom_lint: + mobx_lint: + ``` +- Enable `custom_lint`'s plugin in your `analysis_options.yaml`: + + ```yaml + analyzer: + plugins: + - custom_lint + ``` + +## Running mobx_lint in the terminal/CI + +Custom lint rules created by mobx_lint may not show-up in `dart analyze`. +To fix this, you can run a custom command line: `custom_lint`. + +Since your project should already have custom_lint installed +(cf [installing mobx_lint](#installing-mobx_lint)), then you should be +able to run: + +```sh +dart run custom_lint +``` + +Alternatively, you can globally install `custom_lint`: + +```sh +# Install custom_lint for all projects +dart pub global activate custom_lint +# run custom_lint's command line in a project +custom_lint +``` + +## All assists + +### Wrap widgets with a `Observer` + +![Wrap with Consumer sample](resources/wrap_with_observer.gif) diff --git a/mobx_lint/analysis_options.yaml b/mobx_lint/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/mobx_lint/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/mobx_lint/lib/mobx_lint.dart b/mobx_lint/lib/mobx_lint.dart new file mode 100644 index 00000000..0e3e47fa --- /dev/null +++ b/mobx_lint/lib/mobx_lint.dart @@ -0,0 +1,17 @@ +library mobx_lint; + +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import 'src/assists/wrap_with_observer.dart'; + +PluginBase createPlugin() => _MobxPlugin(); + +class _MobxPlugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => []; + + @override + List getAssists() => [ + WrapWithObserver(), + ]; +} diff --git a/mobx_lint/lib/src/assists/wrap_with_observer.dart b/mobx_lint/lib/src/assists/wrap_with_observer.dart new file mode 100644 index 00000000..105f7554 --- /dev/null +++ b/mobx_lint/lib/src/assists/wrap_with_observer.dart @@ -0,0 +1,45 @@ +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:mobx_lint/src/mobx_custom_lint.dart'; + +import '../mobx_types.dart'; + +/// Right above "wrap in builder" +const wrapPriority = 28; + +class WrapWithObserver extends MobxAssist { + WrapWithObserver(); + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + context.registry.addInstanceCreationExpression((node) { + // Select from "new" to the opening bracket + if (!target.intersects(node.constructorName.sourceRange)) return; + + final createdType = node.constructorName.type.type; + + if (createdType == null || + !widgetType.isAssignableFromType(createdType)) { + return; + } + + final changeBuilder = reporter.createChangeBuilder( + message: 'Wrap with Observer', + priority: wrapPriority, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleInsertion( + node.offset, + 'Observer(builder: (context) { return ', + ); + builder.addSimpleInsertion(node.end, '; },)'); + }); + }); + } +} diff --git a/mobx_lint/lib/src/mobx_custom_lint.dart b/mobx_lint/lib/src/mobx_custom_lint.dart new file mode 100644 index 00000000..6d347253 --- /dev/null +++ b/mobx_lint/lib/src/mobx_custom_lint.dart @@ -0,0 +1,13 @@ +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +abstract class MobxAssist extends DartAssist { + @override + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + SourceRange target, + ) async { + await super.startUp(resolver, context, target); + } +} diff --git a/mobx_lint/lib/src/mobx_types.dart b/mobx_lint/lib/src/mobx_types.dart new file mode 100644 index 00000000..6bb4dc8e --- /dev/null +++ b/mobx_lint/lib/src/mobx_types.dart @@ -0,0 +1,4 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// [TypeChecker] from `Widget` from Flutter +const widgetType = TypeChecker.fromName('Widget', packageName: 'flutter'); \ No newline at end of file diff --git a/mobx_lint/pubspec.yaml b/mobx_lint/pubspec.yaml new file mode 100644 index 00000000..59044f53 --- /dev/null +++ b/mobx_lint/pubspec.yaml @@ -0,0 +1,21 @@ +name: mobx_lint +description: mobx_lint is a developer tool for users of mobx, designed to help stop common issues and simplify repetitive tasks. +version: 1.0.0 +homepage: https://mobx.netlify.app/ +repository: https://github.com/mobxjs/mobx.dart +issue_tracker: https://github.com/mobxjs/mobx.dart/issues + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + analyzer: ">=6.0.0 <7.0.0" + analyzer_plugin: ^0.11.2 + collection: ^1.16.0 + custom_lint_builder: ^0.5.2 + meta: ^1.7.0 + path: ^1.8.1 + mobx: ^2.2.0 +dev_dependencies: + lints: ^2.0.0 + test: ^1.21.0 diff --git a/mobx_lint/resources/wrap_with_observer.gif b/mobx_lint/resources/wrap_with_observer.gif new file mode 100644 index 00000000..441ffd55 Binary files /dev/null and b/mobx_lint/resources/wrap_with_observer.gif differ diff --git a/mobx_lint_flutter_test/pubspec.yaml b/mobx_lint_flutter_test/pubspec.yaml new file mode 100644 index 00000000..5741a333 --- /dev/null +++ b/mobx_lint_flutter_test/pubspec.yaml @@ -0,0 +1,25 @@ +name: mobx_lint_flutter_test +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + mobx: + flutter_mobx: + + +dev_dependencies: + mobx_lint: + path: ../mobx_lint + test: ^1.15.0 + +dependency_overrides: + mobx: + path: ../mobx + flutter_mobx: + path: ../flutter_mobx + diff --git a/mobx_lint_flutter_test/test/all_tests.dart b/mobx_lint_flutter_test/test/all_tests.dart new file mode 100644 index 00000000..69561809 --- /dev/null +++ b/mobx_lint_flutter_test/test/all_tests.dart @@ -0,0 +1,5 @@ +import 'assists/wrap_widget/wrap_widget_test.dart' as wrap_widget_test; + +void main() { + wrap_widget_test.main(); +} diff --git a/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget.dart b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget.dart new file mode 100644 index 00000000..f26ce7e9 --- /dev/null +++ b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + Map(); + + return Scaffold( + body: Container(), + ); + } +} \ No newline at end of file diff --git a/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget_test.dart b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget_test.dart new file mode 100644 index 00000000..751fc82d --- /dev/null +++ b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_widget_test.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:mobx_lint/src/assists/wrap_with_observer.dart'; +import 'package:test/test.dart'; + +import '../../golden.dart'; + +void main() { + testGolden( + 'Wrap with observer', + 'assists/wrap_widget/wrap_with_observer.json', + () async { + final assist = WrapWithObserver(); + final file = File('test/assists/wrap_widget/wrap_widget.dart').absolute; + + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + var changes = [ + // Map + ...await assist.testRun(result, const SourceRange(171, 0)), + + // Scaffold + ...await assist.testRun(result, const SourceRange(190, 0)), + + // Container + ...await assist.testRun(result, const SourceRange(212, 0)), + + // Between () + ...await assist.testRun(result, const SourceRange(222, 0)), + ]; + + changes.forEach((element) { + print(element); + }); + + expect(changes, hasLength(2)); + + return changes; + }, + ); +} diff --git a/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_with_observer.json b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_with_observer.json new file mode 100644 index 00000000..8abe3bf2 --- /dev/null +++ b/mobx_lint_flutter_test/test/assists/wrap_widget/wrap_with_observer.json @@ -0,0 +1 @@ +[{"priority":28,"change":{"message":"Wrap with Observer","edits":[{"fileStamp":0,"edits":[{"offset":228,"length":0,"replacement":"; },)"},{"offset":188,"length":0,"replacement":"Observer(builder: (context) { return "}]}],"linkedEditGroups":[]}},{"priority":28,"change":{"message":"Wrap with Observer","edits":[{"fileStamp":0,"edits":[{"offset":221,"length":0,"replacement":"; },)"},{"offset":210,"length":0,"replacement":"Observer(builder: (context) { return "}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/mobx_lint_flutter_test/test/golden.dart b/mobx_lint_flutter_test/test/golden.dart new file mode 100644 index 00000000..5ef958a8 --- /dev/null +++ b/mobx_lint_flutter_test/test/golden.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:path/path.dart'; +import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; + +@Deprecated('Do not commit') +var goldenWrite = false; + +File writeToTemporaryFile(String content) { + final tempDir = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + + final file = File(join(tempDir.path, 'file.dart')) + ..createSync(recursive: true) + ..writeAsStringSync(content); + + return file; +} + +void testGolden( + String description, + String fileName, + Future> Function() body, + ) { + test(description, () async { + final changes = await body().then((value) => value.toList()); + + try { + expect( + changes, + matcherNormalizedPrioritizedSourceChangeSnapshot(fileName), + ); + } on TestFailure { + // ignore: deprecated_member_use_from_same_package + if (!goldenWrite) rethrow; + + final file = File('test/$fileName'); + + final changesJson = changes.map((e) => e.toJson()).toList(); + // Remove all "file" references from the json. + for (final change in changesJson) { + final changeMap = change['change']! as Map; + final edits = changeMap['edits']! as List; + for (final edit in edits.cast>()) { + edit.remove('file'); + } + } + + file + ..createSync(recursive: true) + ..writeAsStringSync(jsonEncode(changesJson)); + return; + } + }); +} \ No newline at end of file