Skip to content

Add Oref analyzer plugin, lint rules, fixes, and assists#40

Merged
medz merged 44 commits into
mainfrom
feat/analyzer-plugin
Jan 26, 2026
Merged

Add Oref analyzer plugin, lint rules, fixes, and assists#40
medz merged 44 commits into
mainfrom
feat/analyzer-plugin

Conversation

@medz

@medz medz commented Jan 26, 2026

Copy link
Copy Markdown
Owner

Resolves #38

Summary by CodeRabbit

  • New Features
    • Adds an analyzer plugin with multiple lints enforcing hook composition, scope/control-flow rules, BuildContext usage, computed/write safety, plus quick fixes and assists.
  • Tests
    • Adds comprehensive unit tests and a reusable test harness covering the new analyzer rules.
  • Chores
    • Updates project configuration, dependencies, and CI to run the analyzer test suite.
  • Documentation
    • Updates CHANGELOG and README with analyzer/lints guidance.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new Analyzer plugin for oref: introduces a plugin entrypoint, many analyzer rules, fixes, assists, AST utilities (hook detection, scopes, custom-hook discovery, type/package helpers), test harness and tests, CI wiring, and documentation updates.

Changes

Cohort / File(s) Summary
Changelog & Config
CHANGELOG.md, analysis_options.yaml, example/analysis_options.yaml, example/pubspec.yaml, analysis_tests/analysis_options.yaml, analysis_tests/pubspec.yaml, pubspec.yaml
New Unreleased changelog sections; analysis config simplified/converted to plugin-based in example; example oref dependency switched to local path; added analyzer/plugin dependencies; test analyzer config and test pubspec added.
Plugin entry & bootstrap
lib/main.dart, lib/src/analyzer/plugin.dart
New top-level plugin entry (plugin) and AnalyzerPlugin that registers rules, fixes, and assists.
Assists
lib/src/analyzer/assists/*
lib/src/analyzer/assists/add_build_context_argument_assist.dart, lib/src/analyzer/assists/replace_context_with_null_assist.dart, lib/src/analyzer/assists/registry.dart
New assists to add or replace BuildContext arguments and registry to register them.
Fixes
lib/src/analyzer/fixes/*
lib/src/analyzer/fixes/add_build_context_argument_fix.dart, lib/src/analyzer/fixes/replace_context_with_null_fix.dart, lib/src/analyzer/fixes/registry.dart
New fix providers and registry wiring mapping fixes to lint codes (single/multi fixes supported).
Rules & visitors
lib/src/analyzer/rules/*
lib/src/analyzer/rules/registry.dart, lib/src/analyzer/rules/hook_call_visitor.dart, plus avoid_* rules and use_build_context_for_hooks_rule.dart
Multiple new lint rules (context enforcement, custom-hook composition detection, control-flow/nested-function constraints, computed/write checks, effect/scope/discarded-effect checks) and a HookCallVisitorBase; registry registers all warning rules.
Analysis utilities (parts)
lib/src/analyzer/utils/utils.dart, lib/src/analyzer/utils/*
hook_calls.dart, hook_scopes.dart, callback_utils.dart, type_utils.dart, custom_hooks.dart, package_utils.dart
New utility parts for hook invocation detection, hook scope resolution, callback/context checks, type utilities (BuildContext/Widget/WritableSignal detection), custom-hook registry and discovery with caching, and package-level helpers (skip rules for oref package).
Assists/fixes/rules wiring
lib/src/analyzer/assists/registry.dart, lib/src/analyzer/fixes/registry.dart, lib/src/analyzer/rules/registry.dart
Registry functions that wire assists, fixes, and rules into the plugin registry.
Tests & test harness
analysis_tests/*, analysis_tests/test/*, analysis_tests/test/support/oref_rule_harness.dart
New analysis_tests package with a comprehensive test harness (stubs for flutter/oref APIs) and many unit tests covering rules and fixes.
CI
.github/workflows/test.yml
CI extended to run dart pub get and dart test for analysis_tests.
Docs
README.md
README updated with "Analyzer Lints" guidance and plugin enablement instructions.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(240,240,255,0.5)
    actor Dev as Developer
    participant IDE as IDE/Editor
    participant Plugin as AnalyzerPlugin
    participant Registry as PluginRegistry
    participant Analyzer as Analysis Server
    participant Rules as Lint Rules
    participant Utils as Analysis Utils
    end

    Dev->>IDE: Open Dart file / request analysis
    IDE->>Plugin: Load/initialize plugin
    Plugin->>Registry: registerRules(), registerFixes(), registerAssists()
    IDE->>Analyzer: Parse file -> produce AST
    Analyzer->>Rules: Invoke node visitors
    Rules->>Utils: Query hook detection, scopes, types, custom hooks
    Utils-->>Rules: Return HookCall / HookScope / type info
    alt Violation detected
        Rules->>IDE: Emit diagnostic + suggested fixes/assists
        IDE->>Plugin: Request fix/assist edits
        Plugin->>IDE: Provide edits from Fix/Assist providers
        Dev->>IDE: Apply fix
    else No violations
        Rules-->>IDE: No diagnostics
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇
I hopped through ASTs with nimble feet,
I nudged missing contexts so fixes greet.
Rules, assists, and tests in tidy array,
A little rabbit cheering linting day.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main additions: an analyzer plugin with lint rules, fixes, and assists for the oref package.
Linked Issues check ✅ Passed The PR implements substantial portions of issue #38 objectives: analyzer plugin with lint rules, fixes, assists, documentation, and test coverage. Core requirements are met with 9 lint rules, 2 fixes, 2 assists, and comprehensive test support.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the analyzer plugin and lint infrastructure requested in #38. Configuration updates (pubspec.yaml, analysis_options.yaml, example setup) and documentation changes (README, CHANGELOG) are necessary supporting work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@lib/src/analyzer/utils/custom_hooks.dart`:
- Around line 350-399: Both _librarySignature and
_computeCustomHookNamesForLibrary currently use direct File stat/read calls
which bypass analyzer overlays; replace those disk accesses with overlay-aware
AnalysisSession APIs: use the session (e.g., AnalysisSession.getParsedUnit or
getParsedUnit2) for each fragment's path to obtain the ParsedUnitResult.unit and
file timestamp/contents instead of File(...).statSync()/readAsStringSync(),
ensure the analysis context uses an OverlayResourceProvider (or equivalent) so
getParsedUnit reflects unsaved overlays, and hook into overlay change
notifications to invalidate whatever cache uses
_librarySignature/_computeCustomHookNamesForLibrary (referencing the
_hookImportInfoForLibrary call sites) so cached signatures/names are recomputed
when overlays change.

In `@lib/src/analyzer/utils/hook_calls.dart`:
- Around line 77-113: The matchHookConstructor function may dereference
enclosing.library without checking for null; update matchHookConstructor to
defensively handle a null enclosing.library (from the enclosing Element
retrieved from node.constructorName.element) by returning null if
enclosing.library is null before calling _isOrefLibrary—this ensures the check
using _isOrefLibrary(library.uri) is only performed when library is non-null and
avoids a potential null dereference.
🧹 Nitpick comments (4)
lib/src/analyzer/utils/package_utils.dart (1)

30-50: Synchronous file I/O may block the analysis server.

readAsLinesSync() performs blocking I/O on the analysis server thread. While the caching mitigates repeated reads, the initial read could cause latency. Consider whether asynchronous reading is feasible given the analyzer plugin API constraints.

Additionally, a minor optimization: the RegExp(r'\s') on line 42 is recreated on each invocation. Consider extracting it as a top-level constant.

♻️ Suggested optimization
+final _whitespaceRegex = RegExp(r'\s');
+
 String? _readPackageName(String rootPath) {
   final pubspec = File('$rootPath${Platform.pathSeparator}pubspec.yaml');
   if (!pubspec.existsSync()) {
     return null;
   }
   try {
     for (final line in pubspec.readAsLinesSync()) {
       final trimmed = line.trimLeft();
       if (trimmed.startsWith('name:')) {
         return trimmed
             .substring('name:'.length)
             .trim()
-            .split(RegExp(r'\s'))
+            .split(_whitespaceRegex)
             .first;
       }
     }
   } on FileSystemException {
     return null;
   }
   return null;
 }
lib/src/analyzer/utils/callback_utils.dart (1)

148-162: Consider documenting the fallback behavior.

The fallback on line 161 (return parent is Expression ? parent : null) is permissive and could return Expression nodes that aren't direct argument containers. While callers perform additional validation, this implicit behavior could be surprising to maintainers.

♻️ Consider adding a clarifying comment
   if (parent is ParenthesizedExpression &&
       parent.parent is ArgumentList &&
       parent.expression == node) {
     return parent;
   }
+  // Fallback for other expression wrappers; callers validate the full chain
   return parent is Expression ? parent : null;
 }
lib/src/analyzer/utils/hook_scopes.dart (1)

181-201: Name-based heuristic may produce false positives.

The function returns true for any identifier named context before verifying its type. A variable named context that isn't a BuildContext would incorrectly match. The type check on line 200 is the authoritative verification.

Consider reordering to prioritize the type check, or document this as intentional for performance optimization.

♻️ Alternative: prioritize type check
 bool isBuildContextExpression(Expression? expression) {
   if (expression == null) {
     return false;
   }
+  // Prefer type-based check when available
+  if (isBuildContextType(expression.staticType)) {
+    return true;
+  }
+  // Fallback to name heuristic for unresolved types
   if (expression is SimpleIdentifier) {
     if (expression.name == 'context') {
       return true;
     }
   }
   // ... rest unchanged
-  return isBuildContextType(expression.staticType);
+  return false;
 }
lib/src/analyzer/rules/avoid_hooks_in_control_flow_rule.dart (1)

50-106: Consider extracting shared hook‑reporting logic

This visitor largely mirrors the nested‑function rule (hook detection + reporting). A small shared helper/base visitor would reduce duplication and drift as rules evolve.

Comment thread lib/src/analyzer/utils/custom_hooks.dart
Comment thread lib/src/analyzer/utils/hook_calls.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@lib/src/analyzer/utils/custom_hooks.dart`:
- Around line 294-298: Update _normalizeCustomHookElement to use the new
nullable accessor: when element is a PropertyAccessorElement, return
element.variable2 instead of the deprecated element.variable; ensure the
function still returns Element? to accept the nullable variable2 result and keep
the existing fallback return element for non-accessor cases.

In `@lib/src/analyzer/utils/hook_calls.dart`:
- Around line 149-156: The function isOrefFunctionInvocation checks
node.methodName.element but assumes element.library is non-null; add a defensive
null check so that if element is an ExecutableElement but element.library is
null you return false instead of dereferencing it. Update
isOrefFunctionInvocation to verify element.library != null (or obtain uri via a
null-safe check) before calling _isOrefLibrary, keeping the existing
early-return logic and references to node.methodName.element and _isOrefLibrary.
🧹 Nitpick comments (2)
lib/src/analyzer/utils/hook_scopes.dart (1)

175-194: Consider documenting the name-based fallback heuristic.

The fallback to checking identifier names (context) when staticType is null is pragmatic for unresolved types, but could produce false positives if a variable named context has a different type.

Consider adding a brief doc comment explaining when the fallback is triggered and its limitations:

+/// Returns true if [expression] is a BuildContext expression.
+///
+/// Falls back to name heuristics when static type information is unavailable,
+/// which may produce false positives for unrelated `context` identifiers.
 bool isBuildContextExpression(Expression? expression) {
lib/src/analyzer/utils/custom_hooks.dart (1)

3-3: Consider bounded cache or periodic cleanup for long-running sessions.

The global _customHookLibraryCache grows monotonically as libraries are analyzed. While signature-based invalidation prevents stale data, entries for removed/renamed libraries are never evicted. For typical project sizes this is acceptable, but for very long-running analyzer sessions or large monorepos, memory could accumulate.

If this becomes an issue in practice, consider an LRU eviction policy or periodic cleanup.

Comment thread lib/src/analyzer/utils/custom_hooks.dart
Comment thread lib/src/analyzer/utils/hook_calls.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@analysis_tests/test/avoid_hooks_in_nested_functions_test.dart`:
- Line 32: The test returns an abstract Widget via the expression "return
Widget();" which fails analysis; replace that instantiation with a concrete
widget (e.g., return SizedBox.shrink(); or return Container(); or return
Text('')); update the return expression in the test (the line with "return
Widget();") to use one of those concrete widget constructors so the test
compiles and analysis passes.
- Line 49: The test currently attempts to instantiate an abstract Flutter type
with "return Widget();", which is invalid; locate the occurrence of the
expression "return Widget();" in
analysis_tests/test/avoid_hooks_in_nested_functions_test.dart and replace it
with a concrete widget instance (for example return const SizedBox.shrink() or
return Container()) so the test returns a concrete Widget implementation instead
of trying to construct the abstract Widget type.
🧹 Nitpick comments (1)
analysis_tests/test/avoid_hooks_in_control_flow_test.dart (1)

1-53: LGTM!

The tests correctly verify the rule's behavior for hooks inside conditionals vs. top-level hook calls. This enforces consistent hook ordering, which is essential for reactive state management.

Consider adding tests for other control flow constructs (for, while, switch, try-catch) to ensure comprehensive coverage of the rule.

Comment thread analysis_tests/test/avoid_hooks_in_nested_functions_test.dart
Comment thread analysis_tests/test/avoid_hooks_in_nested_functions_test.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/src/analyzer/utils/hook_scopes.dart`:
- Around line 237-252: hookScopeLabel currently only detects custom hooks when
customHooks is non-null, causing inconsistent labels vs enclosingHookScope;
update hookScopeLabel to apply the same custom‑hook predicate regardless of
registry presence by removing the customHooks null guard and using the registry
methods when customHooks is provided otherwise fall back to the existing
heuristic used by enclosingHookScope (i.e., test parent via
isCustomHookFunctionDeclaration and builderFunction via
isCustomHookFunctionExpression equivalently). Ensure you reference
hookScopeLabel, builderFunction, CustomHookRegistry,
isCustomHookFunctionDeclaration and isCustomHookFunctionExpression when making
the change.
🧹 Nitpick comments (1)
analysis_tests/test/avoid_hooks_in_control_flow_test.dart (1)

20-133: Good test coverage for common control-flow constructs.

The test suite covers the primary control-flow cases (if, for, while, switch, try) and includes a positive test for valid top-level usage. The offset calculation via code.indexOf('signal(') and lint span via 'signal'.length are consistent across tests.

Consider adding tests for additional control-flow edge cases.

For completeness, you may want to add tests for:

  • do-while loops
  • else / catch / finally branches
  • Nested control flow (e.g., hook inside if inside for)

These would strengthen confidence that the rule handles all control-flow scenarios.

Example additional test for do-while
void test_reports_hook_in_do_while() async {
  const code = r'''
import 'package:flutter/widgets.dart';
import 'package:oref/oref.dart';

class MyWidget extends StatelessWidget {
  `@override`
  Widget build(BuildContext context) {
    do {
      signal(context, 0);
    } while (false);
    return Widget();
  }
}
''';
  final offset = code.indexOf('signal(');
  await assertDiagnostics(code, [lint(offset, 'signal'.length)]);
}

Comment thread lib/src/analyzer/utils/hook_scopes.dart
@medz medz merged commit 1f5d7c2 into main Jan 26, 2026
3 checks passed
@medz medz deleted the feat/analyzer-plugin branch January 26, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: built-in analyzer plugin for robust oref usage

1 participant