diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..6763c5c --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,15 @@ +#!/bin/bash +# FlutterGuard CI gate — reject commits that introduce high-severity issues +set -euo pipefail + +echo "🔍 Running FlutterGuard static scan..." +dart run flutterguard_cli:flutterguard scan --path . --fail-on high + +if [ $? -ne 0 ]; then + echo "" + echo "❌ FlutterGuard 检测到高优问题,提交被阻止。" + echo " 运行 'dart run flutterguard_cli:flutterguard scan' 查看详情。" + exit 1 +fi + +echo "✅ FlutterGuard 扫描通过。" diff --git a/.gitignore b/.gitignore index 04dcbbf..d2d630d 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ **/linux/flutter/generated_plugin_registrant.cc **/linux/flutter/generated_plugin_registrant.h **/linux/flutter/generated_plugins.cmake + +# FlutterGuard scan output +.flutterguard/ diff --git a/AGENTS.md b/AGENTS.md index b1c7951..90f5f9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,9 +33,11 @@ ```bash dart format . flutter analyze +dart run flutterguard_cli:flutterguard scan --path . --fail-on high ``` - `flutter analyze` 必须通过,不允许有 error 级别问题 +- `flutterguard scan --fail-on high` 必须通过,不允许引入高优问题 - 涉及逻辑代码时补充测试(如有测试框架) - 涉及 UI 教学页时进行人工验收或截图说明 @@ -60,6 +62,14 @@ flutter analyze 5. 标记低质量模块的 `status` 为 `ModuleStatus.pending` 6. 检查 `flutter analyze` 和 `dart format` 是否通过 +## 启用预提交钩子 + +```bash +git config core.hooksPath .githooks +``` + +这会在每次 `git commit` 前自动执行 FlutterGuard 扫描,阻止引入高优问题的提交。 + ## 模块分类枚举 ```dart diff --git a/flutterguard.yaml b/flutterguard.yaml new file mode 100644 index 0000000..4077529 --- /dev/null +++ b/flutterguard.yaml @@ -0,0 +1,42 @@ +include: + - lib/** + - test/** + +exclude: + - lib/**.g.dart + - lib/**.freezed.dart + - lib/**.mocks.dart + +rules: + large_file: + enabled: true + maxLines: 500 + large_class: + enabled: true + maxLines: 300 + large_build_method: + enabled: true + maxLines: 80 + lifecycle_resource: + enabled: true + +boundaries: + # 模块禁止反向依赖 app 层 + - name: no_module_to_app + from: lib/modules/** + forbidden: + - lib/app/** + + # shared 层禁止反向依赖模块或 app + - name: shared_purity + from: lib/shared/** + forbidden: + - lib/modules/** + - lib/app/** + + # shared 层必须纯净,不能反向依赖模块或 app + - name: shared_purity + from: lib/shared/** + forbidden: + - lib/modules/** + - lib/app/** diff --git a/lib/modules/ui/download_animation/pages/paint_animation_page.dart b/lib/modules/ui/download_animation/pages/paint_animation_page.dart index dcb6c12..0643139 100644 --- a/lib/modules/ui/download_animation/pages/paint_animation_page.dart +++ b/lib/modules/ui/download_animation/pages/paint_animation_page.dart @@ -110,7 +110,7 @@ class _PaintAnimationPageState extends State @override void dispose() { for (var item in _flyingItems) { - item.controller.dispose(); + item.dispose(); } super.dispose(); } @@ -445,6 +445,10 @@ class FlyingPaintItem { required this.endPosition, required this.controller, }); + + void dispose() { + controller.dispose(); + } } /// 自定义 Painter 绘制飞行动画 diff --git a/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart b/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart index c8709f7..a69d355 100644 --- a/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart +++ b/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart @@ -17,31 +17,29 @@ class GcodeVisualizerPage extends StatefulWidget { class _GcodeVisualizerPageState extends State with TickerProviderStateMixin { late final GcodePlayerController _controller; - late final TextEditingController _editorController; + final _editorKey = GlobalKey(); @override void initState() { super.initState(); _controller = GcodePlayerController(vsync: this); - _editorController = TextEditingController(text: _controller.source); _controller.parse(); } @override void dispose() { - _editorController.dispose(); _controller.dispose(); super.dispose(); } void _onParse() { - _controller.updateSource(_editorController.text); + _controller.updateSource(_editorKey.currentState!.text); _controller.parse(); } void _onResetSample() { _controller.loadSample(); - _editorController.text = _controller.source; + _editorKey.currentState!.text = _controller.source; _controller.parse(); } @@ -124,7 +122,8 @@ class _GcodeVisualizerPageState extends State return Column( children: [ GcodeEditorPanel( - controller: _editorController, + key: _editorKey, + initialText: _controller.source, onParse: _onParse, onResetSample: _onResetSample, errorCount: _controller.errorCount, diff --git a/lib/modules/ui/gcode_visualizer/widgets/gcode_editor_panel.dart b/lib/modules/ui/gcode_visualizer/widgets/gcode_editor_panel.dart index f85b4db..15e5c12 100644 --- a/lib/modules/ui/gcode_visualizer/widgets/gcode_editor_panel.dart +++ b/lib/modules/ui/gcode_visualizer/widgets/gcode_editor_panel.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class GcodeEditorPanel extends StatefulWidget { const GcodeEditorPanel({ super.key, - required this.controller, + required this.initialText, required this.onParse, required this.onResetSample, required this.errorCount, @@ -11,7 +11,7 @@ class GcodeEditorPanel extends StatefulWidget { required this.hasParsed, }); - final TextEditingController controller; + final String initialText; final VoidCallback onParse; final VoidCallback onResetSample; final int errorCount; @@ -19,15 +19,31 @@ class GcodeEditorPanel extends StatefulWidget { final bool hasParsed; @override - State createState() => _GcodeEditorPanelState(); + State createState() => GcodeEditorPanelState(); } -class _GcodeEditorPanelState extends State { +class GcodeEditorPanelState extends State { + late final TextEditingController _controller; bool _resetPressed = false; bool _parsePressed = false; bool _resetHovered = false; bool _parseHovered = false; + String get text => _controller.text; + set text(String value) => _controller.text = value; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialText); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -104,7 +120,7 @@ class _GcodeEditorPanelState extends State { Padding( padding: const EdgeInsets.all(8), child: TextField( - controller: widget.controller, + controller: _controller, maxLines: 8, style: const TextStyle( fontFamily: 'monospace', diff --git a/pubspec.lock b/pubspec.lock index f45e432..a281f98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.flutter-io.cn" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.7.1" args: dependency: transitive description: @@ -57,6 +73,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" device_info_plus: dependency: "direct main" description: @@ -168,6 +200,28 @@ packages: description: flutter source: sdk version: "0.0.0" + flutterguard_cli: + dependency: "direct dev" + description: + path: "../flutterguard/packages/flutterguard_cli" + relative: true + source: path + version: "0.1.0" + flutterguard_core: + dependency: transitive + description: + path: "../flutterguard/packages/flutterguard_core" + relative: true + source: path + version: "0.1.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" go_router: dependency: "direct main" description: @@ -272,6 +326,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -312,6 +374,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" riverpod: dependency: transitive description: @@ -445,6 +515,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -477,6 +555,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 25febdb..82ae4e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,8 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^4.0.0 + flutterguard_cli: + path: ../flutterguard/packages/flutterguard_cli flutter: uses-material-design: true