import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; /// custom_lint entry point — discovered by the plugin loader. PluginBase createPlugin() => _HaloLintsPlugin(); class _HaloLintsPlugin extends PluginBase { @override List getLintRules(CustomLintConfigs configs) => [ const NoRefInDisposeRule(), ]; } /// Flags any use of `ref` (read/watch/listen/invalidate/etc.) inside the /// `dispose()` method of a class extending `ConsumerState` / /// `ConsumerStatefulWidgetState`. /// /// Why: Riverpod invalidates `ref` the instant `State.dispose()` enters. The /// resulting `Bad state: Cannot use "ref" after the widget was disposed.` is /// caught silently inside `BuildOwner.finalizeTree`, leaves the widget tree /// half-finalized, and the next-pushed screen appears frozen. Real instances /// caught in this repo (2026-05-14): `home_screen.dart`, `payment_screen.dart`. /// /// Fix: move the ref-using cleanup into `deactivate()`, which runs BEFORE /// `dispose()` while `ref` is still valid. See client_app/CLAUDE.md → Pitfalls. class NoRefInDisposeRule extends DartLintRule { const NoRefInDisposeRule() : super(code: _code); static const _code = LintCode( name: 'no_ref_in_dispose', problemMessage: "Don't use 'ref' in dispose(). Riverpod invalidates ref the moment " "dispose() runs; the resulting error is swallowed and silently " "corrupts the widget tree (next screen freezes). Move this cleanup " "to deactivate() instead. See client_app/CLAUDE.md → Pitfalls.", ); @override void run( CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context, ) { context.registry.addMethodDeclaration((method) { if (method.name.lexeme != 'dispose') return; final cls = method.parent; if (cls is! ClassDeclaration) return; // Walk up the supertype chain by name — element resolution is more // robust but text-match is enough for the two real superclasses we // care about, and avoids a full type resolve on every method. final superName = cls.extendsClause?.superclass.name2.lexeme; if (superName == null) return; if (!superName.startsWith('Consumer')) return; method.body.accept(_RefUsageVisitor(reporter)); }); } } class _RefUsageVisitor extends GeneralizingAstVisitor { final ErrorReporter reporter; _RefUsageVisitor(this.reporter); @override void visitSimpleIdentifier(SimpleIdentifier node) { // Match the bare identifier `ref` whenever it's USED (not declared). // Declarations like `final ref = ...` are skipped via inDeclarationContext. if (node.name == 'ref' && !node.inDeclarationContext()) { reporter.atNode(node, NoRefInDisposeRule._code); } super.visitSimpleIdentifier(node); } }