saropa_lints 14.0.3 copy "saropa_lints: ^14.0.3" to clipboard
saropa_lints: ^14.0.3 copied to clipboard

2134 custom lint rules with 254 quick fixes for Flutter and Dart. Static analysis for security, accessibility, and performance.

Changelog #

                                    ....
                             -+shdmNMMMMNmdhs+-
                          -odMMMNyo/-..``.++:+o+/-
                       /dMMMMMM/               `````
                      dMMMMMMMMNdhhhdddmmmNmmddhs+-
                      /MMMMMMMMMMMMMMMMMMMMMMMMMMMMMNh/
                    . :sdmNNNNMMMMMNNNMMMMMMMMMMMMMMMMm+
                    o     ..~~~::~+==+~:/+sdNMMMMMMMMMMMo
                    m                        .+NMMMMMMMMMN
                    m+                         :MMMMMMMMMm
                    /N:                        :MMMMMMMMM/
                     oNs.                    +NMMMMMMMMo
                      :dNy/.              ./smMMMMMMMMm:
                       /dMNmhyso+++oosydNNMMMMMMMMMd/
                          .odMMMMMMMMMMMMMMMMMMMMdo-
                             -+shdNNMMMMNNdhs+-
                                     ``

Made by Saropa. All rights reserved.

Learn more at https://saropa.com, or mailto://dev.tools@saropa.com

2100+ custom lint rules with 250+ quick fixes for Flutter and Dart — static analysis for security, accessibility, performance, and library-specific patterns. Includes a VS Code extension with Package Vibrancy scoring.

Packagepub.dev/packages/saropa_lints

Releasesgithub.com/saropa/saropa_lints/releases

VS Code Marketplacemarketplace.visualstudio.com/items?itemName=saropa.saropa-lints

Open VSX Registryopen-vsx.org/extension/saropa/saropa-lints


14.0.3 #

A new avoid_cascade_shuffle rule catches a subtle bug where (collection..shuffle()).first permanently reorders a shared list just to read one element. Five new pubspec rules review your version-constraint hygiene — flagging an open-ended SDK bound, dependencies pinned to any, and (for applications) ranges so wide the team drifts onto different versions. Turning off Lint integration now actually stops the analyzer. Previously "Lint integration: Off" only flipped an internal flag, so saropa_lints diagnostics kept appearing in the Problems pane. log

Added #

  • New avoid_cascade_shuffle rule (Recommended tier). Flags (collection..shuffle()).first and similar, where ..shuffle() is cascaded onto a stored list whose result is consumed, because shuffle() mutates in place and corrupts the shared collection for every other reader; shuffle a copy instead — (List.of(collection)..shuffle()).first.
  • Five new pubspec version-constraint rules. require_sdk_upper_bound (Recommended) flags an SDK constraint with no upper bound, which lets pub get resolve against an untested future SDK major. avoid_unbounded_dependency (Recommended) flags dependencies pinned to any. require_dependency_lower_bound (Professional) flags constraints with only an upper bound. For applications only (publish_to: none), prefer_caret_constraint_in_app (Professional) suggests ^1.2.3 over the equivalent >=1.2.3 <2.0.0, and avoid_overly_wide_app_constraint (Comprehensive) flags ranges spanning two or more majors. The app-only rules stay silent for published packages, which legitimately need wide ranges.

Fixed (Extension) #

  • "Lint integration: Off" now comments out the plugins: block in analysis_options.yaml so the analyzer stops emitting saropa_lints diagnostics; toggling it back On restores the block with your rule packs and overrides intact. No action required.
  • Drift Advisor anomalies and index suggestions no longer appear twice in the Problems panel when the standalone Saropa Drift Advisor extension is also installed; the Lints integration now defers the Problems publish to that extension while it is active, and resumes if you disable it. No action required.
  • When a Drift Advisor server connects and the standalone Saropa Drift Advisor extension is not installed, a one-time per-workspace toast now recommends installing it for the full Problems-panel experience; it honors the existing proactive-nudge opt-out. No action required.
  • The "Drift Advisor" product name is now shielded from machine translation so it stays in English across every locale instead of being transliterated; affected catalogs correct themselves the next time locales are regenerated. No action required.
Maintenance
  • The locale audit now treats strings that are entirely brand terms, {placeholders}, and punctuation (e.g. Saropa Lints: {message}) as skipped rather than missing, since machine translation can only echo them; this clears two perpetual false-positive coverage gaps.

14.0.2 #

This release introduces a unified multi-pane dashboard that lets you review your project map and code health metrics side by side, alongside an on-demand shortcut to quickly re-check for package updates. It also addresses key interface stability issues, preventing extension host freezes during upgrades and stopping the primary dashboard header from flickering during active analysis. log

Fixed (Extension) #

  • The Findings Dashboard header no longer flickers constantly. The dashboard reloads itself whenever the analyzer republishes diagnostics, and each reload replayed the header's entrance animation — so in an actively-analyzing project the header strobed nonstop. It now skips the reload when nothing you can see has changed, and only animates the header on first open. No action required.
  • The "Upgrading Saropa Lints to X" notification no longer hangs open after you accept an upgrade. The upgrade ran a full project analysis on the blocking call path, which froze the extension host for the whole analysis and left the progress notification (and its Cancel button) unresponsive until VS Code was reloaded. Analysis now runs without blocking and is cancellable, so the notification closes when the upgrade finishes. No action required.

Added (Extension) #

  • New "Saropa Lints: Open Saropa Dashboards" command shows the Project Map and Code Health dashboards side by side on one page. Each pane keeps its full interactive content — the treemap, churn-complexity scatter, and hot-spot table, beside the score status line, KPI filters, and sortable function table — so you can compare where size and complexity concentrate against which functions score worst without switching tabs. Clicking a row opens the file; "Open full screen" on either pane reopens the standalone dashboard. The two standalone commands are unchanged. No action required.
  • Click the "Scanned X ago" pill on the Package Dashboard to rescan and re-check for package updates. The pill is now a button: clicking it refreshes the dashboard and re-runs the pub.dev version check, re-surfacing the "Update available" notification even after you dismissed it. The same action is available from the command palette as "Saropa Lints: Check for Package Updates Now". No action required.

14.0.1 #

The Project Map dashboard now hides machine-generated and localization files from its size map and hot-spot rankings, so the files it surfaces are ones you can actually improve. Previously a single generated database file or a megabyte of translation tables would dominate the list and bury the real issues. log

Fixed #

  • The Project Map dashboard no longer ranks generated and localization files in its size map and hot spots. Files emitted by code generators (.g.dart, freezed, drift, auto_route, injectable, protobuf, and similar) and the app_localizations* / intl_* translation tables now stay out of the rankings, matching the Code Health dashboard's existing behavior. These files are long, mechanical, and unimprovable, so they crowded out the hand-written code that hot spots are meant to highlight. No action required.

Changed #

  • The warnings for require_keyboard_visibility_dispose, avoid_openai_key_in_code, and require_speech_stop_on_dispose now spell out the failure each prevents. The three messages were far shorter than the rest of the catalog and stopped at the symptom; they now describe the leaked widget, the billable key abuse, and the held microphone in full. No action required.

  • The Saropa dashboards now share one consistent visual style, and the Project Map follows your editor theme. The Project Map dashboard previously rendered a fixed palette that ignored your light / dark / high-contrast theme; it now tracks the active theme in the editor (while standalone HTML reports keep their styled palette). The Code Health scanning screen, the About panel, the rule-violations dashboard, the command catalog, and the package-details sidebar were all brought onto the shared design system, so colors, spacing, and type match across every surface. No action required.

Maintenance
  • Removed three orphaned localization keys left by the Suggestions sidebar removal. configSuggestions.packAvailable, configSuggestions.initMissing, and configSuggestions.badgeTooltip were only ever read by the deleted Suggestions tree provider; no code referenced them after that view was removed. Dev-only.
  • Generated-file detection is now one shared predicate, used by every CLI. The list of code-generator suffixes plus gen-l10n table detection lived inline in several scanners; it is now a single isGeneratedDartPath helper the analysis CLIs share, so extending the list updates every consumer at once. The cross-file analyzers (unused symbols, duplicates, unused l10n, missing mirror tests), project_vibrancy, and the Project Map size scanner all delegate to it; the predicate also recognizes a generated path segment and the full codegen-suffix set, so each consumer now agrees on what counts as generated. Dev-only.
  • Added a cross-project dashboard style guide. A canonical design-system document (docs/design/SAROPA_DASHBOARD_STYLE_GUIDE.md) defines one token set, component contract, and accessibility gate for every Saropa dashboard surface, so the extension's dashboards stop diverging into separate visual styles. Docs-only.
  • The shared dashboard chrome now carries the full token scale, and the six non-conforming surfaces adopt it. dashboardChromeStyles.ts gained the spacing / radius / type / elevation / motion / z-index tokens (plus an exported getDashboardTokens() for surfaces that keep bespoke components), and the About panel, Code Health scan screen, Project Map, rule-violations dashboard, command catalog, and package-details sidebar were re-tokened onto it — bespoke layouts (the score gauge, command tiles, package badges, scan stepper) kept, only their values converged. Dev-only.
  • Clarified the dashboard style guide's scope after first adoption. Added an explicit exemption for high-density log/terminal consoles, a note that VS Code collapses the four-step surface ramp onto two host backgrounds, and brought Saropa Log Capture's dashboard webview panels into the per-platform adoption section. Docs-only.
  • Adopted the style guide's button, badge, and grade-color hardening rules. Secondary buttons in the shared chrome now carry a fallback fill and a guaranteed border, so host themes that leave --vscode-button-secondaryBackground undefined no longer render buttons as bare text; and letter grades across the Code Health report and the scan screen now drive off one shared A–F ramp derived from the semantic tokens instead of per-surface grade colors. Dev-only.
  • Reconciled the style guide body with its VS Code reference implementation — the surface-0 caveat, 13px type base, and standalone --brand-glow value now match chromeTokens() instead of disagreeing with it. Docs-only.
  • Began a consolidated "Saropa Dashboards" view that shows Project Map and Code Health on one page. A new saropaLints.openDashboards command opens a host webview that embeds each dashboard's full interactive report in its own iframe, side by side, preserving every chart and interaction (no summarizing). The Project Map pane plus the iframe drill-down message bridge are in place; the Code Health pane follows once the iframe mechanism is confirmed in the Extension Development Host. The standalone Project Map and Code Health commands are unchanged. Dev-only until complete.
  • Added instantiation-pin tests for six package rule packs that had none (Envied, Keyboard Visibility, Google Fonts, OpenAI, Speech to Text, uuid), fixing the tier-integrity check that requires every rule category to carry a test and closing the gap that let a sub-standard message ship unnoticed. Dev-only.
  • A release-commit push to main no longer triggers a redundant ci run. The publish workflow already validates that exact tagged commit and the publish script mirrors the full gate locally, so cutting a release stops firing three overlapping workflows at once. Dev-only.
  • Closed the stub-test tracking plan by dropping its deferred follow-up scope. The plan's stub removal and the hard zero-gate that keeps empty-body test/testWidgets stubs out are complete and verified; the never-started "rewrite removed stubs as fixture-backed tests" backlog was removed as not needed. Docs-only.

14.0.0 #

Hardens the release pipeline so a missing dependency can no longer reach pub.dev, and teaches the Package Vibrancy dashboard to name the real package behind a stuck upgrade when two dependencies fight over a shared transitive dependency. Also lays the foundation for the Saropa suite: the extension now exports its findings to a shared file the Drift Advisor and Log Capture extensions can read, and contributes stable deep-link commands so those tools can jump into a rule. Replaces the catch-all "Mixed packages" rule pack with one focused pack per package (OpenAI, uuid, Envied, Google Fonts, and others) so you can enable just the rules for packages you actually use, and reworks the rule-packs dashboard — renamed Manage Rule Packs — with an A–Z table, an inline rule list, a one-click "Enable all recommended packs" action, and the noisy Suggestions sidebar removed. The only action required is for projects that enabled the old package_specific pack (see Changed). Polishes the extension dashboards too, so their filters can be driven from the keyboard and their colors follow your editor theme under light and high-contrast modes. log

Added #

  • Six focused rule packs replace the catch-all "Mixed packages" bucket: OpenAI, uuid, Envied, Google Fonts, Keyboard Visibility, and Speech to Text. Each gates on its own dependency, so enabling one no longer pulls in rules for packages you do not use; the rules themselves are unchanged. If you enabled package_specific, see Changed for the migration.

Changed #

  • The package_specific ("Mixed packages") rule pack was removed and its rules split into per-package packs. The 19 unrelated rules it bundled now live in dedicated packs gated by the package each targets (existing packs like app_links, image_picker, webview_flutter, url_launcher, geolocator, google_sign_in, firebase, plus the six new packs above). If your analysis_options.yaml lists package_specific under rule_packs.enabled, replace it with the specific packs you want — the old id is now ignored, so those rules turn off until you do.

Fixed #

  • A standalone // ignore: placed on its own line directly above a ternary ?/: branch is now honored by the headless scanner. Previously only a trailing same-line ignore suppressed a diagnostic reported on a ternary operand, so a correctly-placed leading ignore was silently dropped; add the ignore above the branch as expected and it now applies. No action required.

  • avoid_bloc_event_in_constructor no longer flags list.add(...) or controller.add(...) in a Bloc constructor. It flagged every method named add, so populating a local list or stream controller during construction was wrongly reported as dispatching a Bloc event; it now flags only an unqualified add(event) on the Bloc itself. No action required.

  • avoid_ref_in_dispose and avoid_ref_inside_state_dispose no longer flag an unrelated field or local named ref used in dispose(). They reported any ref by name, so a non-Riverpod object named ref was wrongly flagged; they now report only when ref resolves to Riverpod's WidgetRef/Ref. No action required.

  • avoid_nullable_async_value_pattern no longer flags .value on a variable merely named with "async". It matched the variable name, so any asyncThing.value on an unrelated type was wrongly reported; it now flags only .value on a real AsyncValue. No action required.

  • require_immutable_bloc_state no longer flags a ...State-named class that is not a Bloc state. Any concrete class whose name ended in State (such as a RequestState value object) was flagged purely by name; it now fires only when the class is used as a Bloc/Cubit state type argument. No action required.

  • prefer_cubit_for_simple now counts event handlers from the code, not from comments or strings. The handler count came from a text scan that matched on<...> tokens inside comments and string literals and missed nested generics like on<Wrapper<int>>; it now counts real on<Event>(...) registrations, fixing both over- and under-counting. No action required.

  • avoid_logging_sensitive_data no longer flags logging an OAuth flow identifier such as oauthToken. The intended OAuth carve-out never worked and the rule treated the token inside oauthToken as a leaked secret; it now suppresses only when the sensitive word is part of a known safe identifier. No action required.

  • prefer_streaming_response no longer flags .body or in-memory uses of .bodyBytes. It also fired on the String .body and on any .bodyBytes that merely shared a scope with an unrelated file variable; it now flags only .bodyBytes written to disk in the same function. No action required.

  • avoid_over_fetching no longer flags ordinary repository methods that fetch and return a single field. A short method like final r = await api.fetch(id); return r.id; was reported with no real over-fetch signal; it now fires only when a fetched object's single field feeds another call (such as a widget). No action required.

  • require_token_refresh no longer double-flags an auth class and no longer fires when refresh logic is present. A class that stored an access token alongside a refresh token and refresh method was still flagged for lacking an inline expiry check; the rule now reports once, only when no refresh signal exists at all. No action required.

  • avoid_jwt_decode_client no longer flags constructing a JWT model object outside an authorization check. Building a value like JsonWebToken.fromMap(...) for display or storage was wrongly reported; the rule now requires the same role/permission decision context as its method-call form. No action required.

  • require_biometric_fallback now catches the canonical localAuth.authenticate(biometricOnly: true) call. The receiver-name match missed single-token receivers like localAuth, so biometric-only logins went unflagged; it now matches the local_auth biometric API. No action required.

  • avoid_webview_javascript_enabled no longer fires when JavaScript is disabled but an unrelated flag (e.g. isInspectable: true) is set. The rule now reads the actual boolean value of javaScriptEnabled instead of scanning the argument text for true, so InAppWebViewSettings(javaScriptEnabled: false, isInspectable: true) stays silent. No action required.

  • prefer_cached_getter no longer flags repeated plain field reads. It treated any property accessed twice in a method (widget.title, a struct field) as an expensive getter to cache, even though a plain field read is a direct memory fetch. It now fires only when the repeated access resolves to an explicitly-declared get accessor; a field's implicit (synthetic) getter is skipped. No action required.

  • Ten widget-pattern rules stopped firing on false matches by checking resolved types and code structure instead of names and text. avoid_stateful_widget_in_list now flags only StatefulWidget list items, avoid_duplicate_widget_keys treats Key('x') and ValueKey('x') as distinct, avoid_gesture_conflict requires direct nesting, prefer_tap_region_for_dismiss no longer trips on populate/closest, prefer_asset_image_for_local matches only assets/ paths, avoid_nullable_widget_methods no longer flags non-widget types named "…Widget", avoid_double_tap_submit no longer treats a "Reorder" button as submit, avoid_late_without_guarantee detects a real initState assignment, avoid_static_route_config matches exact router types, and prefer_split_widget_const counts only const-constructible children. Fewer spurious warnings; no action required.

  • avoid_string_concatenation_loop no longer flags numeric += accumulators. A loop body like total += count or resultMap[k] += n was reported as O(n²) string concatenation based only on the variable name containing result/output/buffer/message. The += branch now requires the accumulator to actually be a String (by resolved type), so numeric accumulation stays silent while genuine String += ... still reports. No action required.

  • Several widget-lifecycle rules now check resolved AST structure and element identity instead of text/lexeme matching. require_super_dispose_call, require_super_init_state_call, and avoid_set_state_in_dispose were silently dead — they tested a method's direct parent for ClassDeclaration, but that parent is the class body node, so they never fired; they now resolve the enclosing State subclass and report again. avoid_inherited_widget_in_initstate and avoid_expensive_did_change_dependencies no longer fire on a same-named initState/didChangeDependencies method declared on a plain (non-State) class. avoid_unsafe_setstate no longer treats setState() in the else branch of if (mounted) as guarded. prefer_widget_state_mixin no longer counts unrelated members like _unfocusTimer/_focusableItems as interaction-state fields (whole-word, bool-typed match). avoid_scaffold_messenger_after_await walks the body in source order so a ScaffoldMessenger.of(context) call lexically before the await is not reported as "after". require_field_dispose detects disposal via real cascade sections, so a sibling field disposed inside another field's cascade argument no longer satisfies the wrong field. require_scroll_controller_dispose skips controllers read from widget.* (parent-owned). avoid_recursive_widget_calls matches self-instantiation by resolved element, so a same-named imported widget is no longer flagged. require_init_state_idempotent matches removeListener/removeObserver as a real invocation instead of a source substring. No action required.

  • Seven widget-layout rules now use resolved AST checks instead of source-text or name matching. avoid_opacity_misuse flags only a state-toggled ternary, so a static const such as opacity: _kDisabledOpacity stays silent; prefer_fractional_sizing matches a real MediaQuery…size.width|height read by structure, so a same-named local like fakeMediaQuery.size.width * 0.5 no longer trips; prefer_page_storage_key recognizes a PageStorageKey by type and is no longer fooled by lookalike names; avoid_stack_without_positioned skips children of a Stack that sets its own alignment/fit (the intentional badge-over-avatar pattern); prefer_spacing_over_sizedbox treats SizedBox(height: 8) and 8.0 as the same spacer; avoid_builder_index_out_of_bounds compares full receiver chains so a guard on other.items.length no longer excuses items[index], catching a real out-of-bounds risk; and avoid_hardcoded_layout_values drops a dead duplicate branch with no behavior change. No action required.

  • require_stream_error_handling no longer flags a non-Stream object whose name ends in "controller". A call like animationController.listen(...) was reported for a missing onError because the name-based fallback treated any "…controller" receiver as a stream. It now requires the resolved Stream type, an explicit .stream access, or a name ending in "stream"; a bare AnimationController/ScrollController stays silent. No action required.

  • prefer_utc_for_storage no longer flags a toIso8601String() used for a UI label when an unrelated storage call is in the same method. The context scan ran storage patterns against every enclosing node's full source, so any save(/insert(/.set…( elsewhere in scope marked a display-only timestamp as "storage". The scan now stops at the immediate enclosing statement, so only the statement that actually persists the value is considered. No action required.

  • handle_throwing_invocations no longer flags an unrelated dart:io call such as systemEncoding.decode(...). A catch-all final clause matched any dart:io element whose method name appeared in the throwing-call set, so non-I/O members named decode/parse were wrongly reported; detection is now restricted to the documented throwing calls qualified by both library and method (dart:io read*/write* sync I/O, dart:convert decode/jsonDecode, dart:core parse). No action required.

  • avoid_dialog_context_after_async no longer misjudges a mounted check by position when comments or whitespace precede the pop. The mounted-check scan sliced a re-rendered source string at original-source offsets, so a guard placed after Navigator.pop(context) could be credited as guarding it (and a real one before it missed). It now compares AST node offsets directly, so a pop guarded only by a later mounted check is correctly flagged. No action required.

  • prefer_no_commented_out_code no longer flags a wrapped prose sentence that cites a function call. A multi-line comment such as "…Without this, formatNumberLocale(x, decimalPlaces: 25) crashed (formatDouble in…" was flagged on the middle line because it names a call, even though it is mid-sentence English; the rule now treats a lowercase continuation line carrying English function words or a dangling parenthesis as prose, while genuine commented-out code under a prose lead-in still reports. The same change fixes comment lines containing prefix, suffix, fixture, or toStringAsFixed being misread as FIX task markers (marker words now match only as whole words), which also affects prefer_capitalized_comment_start. No action required.

  • prefer_setup_teardown no longer flags subject construction that is parameterized per test across a group. When three tests build the subject with the same literal (AsyncSemaphoreUtils(1)) while sibling tests in the same group build it with different literals ((2), (3)), the construction is per-test arrange, not a hoistable fixture — extracting one variant into setUp() would leave the others constructing locally. The rule now collapses call shapes that differ only by a literal argument and suppresses when more than one variant exists in the group, while genuinely identical setup still reports. No action required.

  • require_route_guards no longer fires on routes whose path merely contains a protected word. This ERROR-level rule treated /reorder as the protected segment order and /accounting as account (substring match), demanding an auth guard on unprotected routes. It now matches whole /-split path segments, so only a route segment actually named e.g. account or admin is flagged. No action required.

  • avoid_go_router_push_replacement_confusion no longer flags non-detail routes by substring. context.go('/viewport/$x') matched the detail segment /view and was reported; matching is now per path segment, so /viewport no longer trips the view rule. No action required.

  • avoid_push_replacement_misuse no longer fires on builder bodies and route objects. It lowercased the whole argument source, so MaterialPageRoute(builder: () => ListView()) matched view and an Order… builder matched order. It now inspects only the route name (the pushReplacementNamed string and RouteSettings(name:)) by segment. No action required.

  • require_deep_link_testing no longer treats uuid:/valid: as an id. The rule checked whether the route argument source contained id:, so an object built with a uuid: or valid: argument was wrongly considered to carry a routable id and skipped. It now inspects argument labels and map keys for an exact id, so objects lacking one are correctly flagged. No action required.

  • avoid_pop_without_result no longer confuses similarly-named variables. A result from await Navigator.push(...) was considered "used without a null check" when an unrelated resultValue appeared nearby, and its null guards were missed for the same reason; it now tracks the variable by element identity and recognizes == null / != null / ! / ?. handling. No action required.

  • prefer_layout_builder_for_constraints no longer flags two legitimate MediaQuery sizing patterns. It fired on a MediaQuery.sizeOf read that is the fallback branch of a constraint-finiteness guard (constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.sizeOf(context).width) — code that already uses LayoutBuilder and only consults MediaQuery when the parent passes unbounded constraints, where there is no LayoutBuilder value to use. It also flagged a screen-fraction height scaled by a named factor (MediaQuery.sizeOf(context).height * fraction) inside an unbounded scroller, where LayoutBuilder would yield infinite constraints; the screen-fraction exemption previously required a numeric literal and now accepts any *// factor. Breakpoint comparisons still require a numeric literal, so genuine width: MediaQuery.sizeOf(context).width sizing keeps flagging.

  • Line-level // ignore: no longer over-suppresses rules whose name is a prefix of another. An // ignore: my_rule_extended comment was matched by a bare substring check, so it also silenced the distinct, shorter my_rule on the same line or declaration — hiding real diagnostics. Matching is now whole-word (\b-anchored), consistent with // ignore_for_file:. If you relied on a single ignore accidentally covering a prefix-named rule, add that rule to the comment explicitly.

  • Baseline files no longer suppress violations in the wrong file when two filenames share a suffix. A baseline entry for util.dart was matched against my_util.dart (and any path merely ending in the same characters) because path comparison used an unbounded suffix check, hiding real violations in unrelated files. Suffix matching now requires a path-segment (/) boundary, so relative-vs-absolute paths still match but distinct files do not.

  • avoid_future_tostring no longer fires on FutureOr values, and the Future-handling rules now recognize Future subtypes. Several async rules (avoid_future_ignore, avoid_future_tostring, prefer_return_await, avoid_unawaited_future) decided "is this a Future?" by checking whether the type's display name starts with Future — which wrongly flagged FutureOr<T> (a false positive on .toString()/interpolation) and missed types that implement Future. They now use a proper type check (Future or a Future subtype, excluding FutureOr), removing the false positives and catching previously-missed Future subtypes.

  • avoid_expensive_build no longer flags cheap built-in conversions in build(). int.parse, double.tryParse, DateTime.parse, and Uri.parse were matched purely by the method name parse/tryParse, so these common, inexpensive calls were reported as expensive build-time work. The rule now skips parse/tryParse whose resolved result is a core primitive while still flagging heavy parsing such as jsonDecode.

  • incorrect_firebase_event_name no longer fires on non-Firebase analytics. This ERROR-level rule flagged any logEvent(name: ...) call against Firebase's naming rules, but Mixpanel, Segment, and Amplitude expose the same method with different rules. It is now gated to files that reference Firebase.

  • require_database_index and require_database_migration no longer fire in projects with no embedded database. Their query/model heuristics matched ordinary in-memory collection access (.users, .items, .where(...)) and unrelated model classes; both are now gated to files that reference Isar/Realm/ObjectBox (index) or Hive/Isar (migration).

  • avoid_drift_unsafe_web_storage no longer fires without a Drift import. It flagged any WebDatabase(...) constructor or any method named unsafeIndexedDb, even in code that does not use Drift; it now requires a drift/drift_flutter import like every other Drift rule.

  • avoid_throw_in_catch_block now runs everywhere, not only in BLoC files. This general reliability rule (throwing in a catch block without preserving the stack trace) was restricted to files containing a BLoC/Cubit, so it was effectively inactive for most code despite being a Recommended-tier rule. It now applies to all Dart files; if this surfaces more findings than wanted, use rethrow / Error.throwWithStackTrace or disable the rule.

  • Android permission checks no longer match a longer permission that shares a prefix. A check for READ_CONTACTS also matched a manifest declaring only READ_CONTACTS_EXTENDED, so manifest-based rules could mis-detect a granted permission. Matching is now boundary-anchored to the exact permission name.

  • Android manifest edits are now picked up without restarting the analyzer. The manifest was cached per project for the whole analysis-server session with no invalidation, so adding a permission or service didn't change results until restart. The cache now refreshes when the manifest's size or modification time changes (same approach already used for iOS Info.plist).

  • iOS background-mode detection no longer misreads an unrelated audio/location string. hasIosBackgroundAudioConfigured/...LocationConfigured checked for the mode string anywhere in Info.plist, so a plist enabling only background location that mentioned audio elsewhere reported audio as enabled. The mode is now matched inside the UIBackgroundModes array.

Added (Extension) #

  • Package Vibrancy now flags ~40 more abandoned packages and points each at its maintained successor. New cross-grades cover Community Edition databases (isar/isar_flutter_libsisar_community/isar_community_flutter_libs, hive_flutterhive_ce_flutter), the last Flutter Community Plus gap (wifi_info_flutternetwork_info_plus), discontinued packages pub.dev itself names a replacement for (the AngularDart packages → ng*, uni_linksapp_links, artemisferry, super_enumfreezed, and more), and abandoned widgets/plugins (flutter_web_authflutter_web_auth_2, pdf_renderpdfrx, nfc_in_flutternfc_manager, others). Most offer a one-click "Replace with…" pubspec fix; no action required.

  • A production crash can now point you at the lint rule that would have prevented it. When the Saropa Log Capture extension records a runtime crash (a "No element", a null-check-operator failure, a RangeError, and others) and the Saropa Lints rule that catches that class is currently disabled, a one-time prompt offers to enable that rule. The prompt appears at most once per rule and only when Log Capture's diagnostics file is present, so it never nags. No action required if you do not use Log Capture.

  • The Config dashboard now has a "Style & opinions" section for the opt-in stylistic rules. The ~220 stylistic rules — off in every tier, previously only reachable through the setup wizard — are grouped by concept (naming, formatting, ordering, widget style, and more) in a collapsed accordion below the packs. Conflicting choices (single vs double quotes, early-return vs single-exit, and the rest) render as pick-one radios so two contradictory rules can never be enabled at once; the rest are independent toggles with enable-all / disable-all per group. Each group carries a one-line description — including a noisiness warning where it matters (Ordering, Naming, Formatting) — so you can tell a quiet rule from a reformat-everything one before turning it on. Turning a rule on writes a rule: true override; turning it off removes the override. Off by default — nothing changes until you opt in.

  • The packs list is split into "For your project" and "All packages", and the full catalog is grouped by domain. Packs whose dependency or SDK matches your pubspec open by default in a short, relevant list; the full catalog sits in a collapsed accordion below, sub-grouped into collapsible domains (State management, Networking & APIs, Storage & persistence, Navigation & deep links, Media & graphics, Device & platform, Identity & sharing, Utilities & config, and SDK migrations) so you can browse by problem area instead of one ~80-row list. Each domain shows a one-line description, and detected packs carry a domain chip so their area is visible at a glance. Search and column sorting span every group, and a search auto-opens the groups that contain a match; no action required.

  • Static findings now export to a shared file the Saropa Drift Advisor and Log Capture extensions can read. After analysis settles, the extension writes its current findings to .saropa/diagnostics/lints.json (the Saropa Diagnostic Envelope) so the sibling tools can correlate your live database and runtime behavior against the rules that govern them; no action required, and nothing leaves your machine.

  • Sibling tools can deep-link straight to a rule, enable it, or open a finding. The extension contributes stable saropaLints.explainRule, saropaLints.enableRule, and saropaLints.openFinding commands so a Drift Advisor or Log Capture suggestion can jump into Rule Explain, turn on the covering rule, or open the exact source line; no action required.

  • The dashboard now badges a rule when a sibling tool confirms it at runtime. When Drift Advisor or Log Capture has written its mirror and points a runtime issue at one of your rules, that rule's row gains an "Advisor confirms at runtime" or "Log Capture saw N" badge, so you can see which static findings are backed by live evidence; appears only when a sibling extension is installed and active.

  • A Drift finding can jump straight to its live runtime issues in Drift Advisor. The editor lightbulb on a Drift-rule finding now offers "Show live Drift issues (Drift Advisor)" when the Drift Advisor extension is installed, so you can confirm a static finding against the running database in one click; the action is hidden when that extension is absent.

  • Projects paired with Drift Advisor are pointed at the third suite tool, once. A project that dev-depends on saropa_drift_advisor but does not have the Log Capture extension shows a single suggestion to install it (so runtime SQL can be correlated with static findings); it is gated per workspace and respects the proactive-nudge opt-out, so it never nags.

  • Exported findings are stamped with the current commit, so the suite tools can line up per commit. Each diagnostic written to the shared file now carries the workspace's commit SHA, letting Drift Advisor and Log Capture correlate "at this commit, the code had these findings" against their own runtime data; resolved by reading .git (no git process is spawned) and omitted outside a git checkout.

  • Package Vibrancy now explains shared-dependency (diamond) upgrade blocks, including SDK pins. When a package is held back because a sibling caps a shared transitive dependency it needs — dart_style stuck because another dependency pins analyzer low, or a package pinned by the Flutter SDK's exact characters/collection/meta — the dashboard names the blocker, the shared dependency, and the resolvable-vs-latest gap instead of showing an unexplained block; no action required.

  • constrained packages now name the constraint holding them back. A package the resolver could lift but your own pubspec caps now shows "your constraint ^x caps this — y resolvable" so the line to edit is obvious, instead of a bare "constrained" label; no action required.

  • Git, path, and SDK dependencies no longer show as stuck pub upgrades. A version gap on an overridden or SDK dependency is annotated as managed (and its "update available" squiggle suppressed), because such deps can't be bumped by editing a constraint; no action required.

  • Deliberately-pinned dependencies are marked as intentional holds. A "do not bump" / "do not use" note in a dependency's pubspec comment is read and shown as a pin, and its upgrade nag suppressed, so a frozen dependency reads as a decision rather than neglect; no action required.

  • Cross-project version drift against sibling repos. Set saropaLints.packageVibrancy.siblingRepoPaths to other repo folders and a package pinned at a lower major than a sibling (e.g. saropa_lints ^9.7.0 here vs ^13.12.7 elsewhere) is flagged as behind, surfacing a lagging consumer pub-outdated can't see; off until paths are configured.

  • The Package Vibrancy dependency graph now zooms, pans, and opens collapsed to direct dependencies. It starts showing only your direct dependencies (so a large project's graph is readable on open) with an "Expand transitives" button to reveal the full depth-2 view; drag to pan, use the zoom buttons or mouse wheel to zoom, "Reset view" to refit, and clicking a node centers it before jumping to its table row. No action required.

  • The package screen now shows a consolidated changelog of every release between your version and the latest. Release notes are scraped (the package's CHANGELOG first, pub.dev as fallback) and rendered inline so you can read what actually changed before adopting a newer version, rather than upgrading blind; no action required.

Changed (Extension) #

  • The "new versions available" notification no longer offers a one-click "Update All". Bulk-pulling every newest version from an unsolicited toast, without reviewing each changelog, is a supply-chain risk (a freshly-published malicious release would be adopted across your whole dependency graph in one tap); the toast is now awareness-only ("View Details" / "Dismiss") and you upgrade per package from the package screen, where the new consolidated changelog makes each upgrade a reviewed decision. No action required.
  • The Package Vibrancy dashboard now shows a package's full detail in a docked side pane. Selecting a row opens its versions, community metrics, alerts, platforms, and links in a master-detail pane beside the table — the rich detail that previously required a separate editor tab — so you can scan the list and read detail in one place; the pane is hidden until you select a row, and closes with its × or Escape. No action required.
  • The Lints Config dashboard is now "Manage Rule Packs" and opens sorted A–Z by pack name. The clearer name surfaces the powerful, easily-missed package packs, and name-sorting makes the long pack list scannable instead of ordering by an opaque rule count; no action required.
  • The pack table leads with a "Recommended" column and every header now has an explanatory tooltip. The old mid-table "In pubspec" column moved to the front and was renamed, so the packs that apply to your project (their dependency or SDK gate is satisfied) are the first thing you see; no action required.
  • A pack's rules now expand inline instead of opening a popup, and each rule is a link to its explanation. Clicking "View" reveals the rule list in a row beneath the pack and each rule opens Rule Explain, so you can inspect a pack without losing the table; no action required.
  • New "Enable all recommended packs" button turns on every pack your project's dependencies and SDK satisfy in one click. It enables package packs whose dependency is present, SDK packs your environment satisfies, and applicable version-upgrade packs, after a confirmation; no action required.
  • The pack-type filter dropdown is now readable in dark themes. Its options bound to your editor's dropdown theme colors instead of rendering low-contrast gray-on-black; no action required.

Removed (Extension) #

  • The standalone "Suggestions" sidebar panel was removed. Its long "Enable the X rule pack" list was noise; applicable packs are now surfaced by the single startup notification (whose action opens Manage Rule Packs) and the dashboard's "Enable all recommended packs" button. No action required.

Fixed (Extension) #

  • Package Vibrancy no longer mislabels finished first-party packages (e.g. path_provider) as merely "outdated". A mature flutter.dev/dart.dev/google.dev/firebase.google.com package that publishes rarely because it is complete could score into the "outdated" band on low repository churn; trusted publishers are now lifted one band to "stable" (matching the existing "stable" → "vibrant" promotion), while genuinely dead packages are untouched. No action required.

  • The Package Vibrancy dependency graph now follows the table's filters. It rendered once from the full dependency set and ignored the search box, age slider, dev-dependencies toggle, presets, and chart filters, so it showed packages you had filtered out; it now re-renders to match the visible rows, dropping hidden packages and any edge to one. No action required.

  • A short package-update poll interval can no longer run away. Setting saropaLints.packageVibrancy.watchIntervalHours to 0 (or a negative/invalid value) created a zero-millisecond timer that polled the pub registry continuously; the interval is now floored at 15 minutes and falls back to 6 hours for invalid values.

  • Translations that are intentionally empty are now respected. The localization lookup treated an empty-string translation as "missing" and fell back to English; it now distinguishes a missing key from an intentionally blank value.

  • Findings dashboard sections now open compact again. Every group except the first re-collapses on load as intended, instead of all sections expanding, so a large report opens scannable rather than as one long wall; no action required.

  • Dashboard filters are now fully keyboard-accessible. The Package Vibrancy summary cards focus with Tab and toggle their filter on Enter or Space, and a visible focus ring was added to the package-comparison Add buttons, the rule-triage actions, and the project-vibrancy score-threshold input, so the dashboards can be driven without a mouse; no action required.

  • Several dashboard surfaces now follow light and high-contrast themes correctly. Package-status badge text, the Findings dashboard top-rules text, and a few control borders and focus rings were bound to editor theme colors instead of fixed values that could wash out or disappear in the opposite theme; no action required.

  • Command Catalog category headers stay visible while scrolling. The sticky section label now pins just beneath the toolbar instead of sliding behind it, so you can always see which category you are scrolling through; no action required.

  • Dashboards no longer overflow sideways in a narrow editor pane. The Package Vibrancy, Known Issues, and package-comparison tables now scroll horizontally within their own bounds and the package-detail link strip wraps, so a docked or split-narrow webview shows the content instead of a whole-page horizontal scrollbar; no action required.

  • Dashboards are now navigable by screen reader. Each editor dashboard exposes a single banner and one main landmark (a duplicate nested <header> is gone), wraps its summary and controls in labeled regions, gives the icon-only table columns accessible names, corrects a skipped heading level in the empty state, underlines comparison links so they are not distinguished by color alone, and styles the Findings skip link so it stays hidden until focused; no action required.

  • Dashboard text now meets WCAG AA contrast in every theme. Status pills, category labels, muted card labels, the "Unused" badge, and the footprint/filter toggles were lifted off colors that dipped under the contrast minimum on light, dark, or high-contrast themes — the semantic hues are preserved (and the large KPI numbers, grade badges, and charts keep their full color) so the dashboards read clearly without losing their look; no action required.

Maintenance
  • The publish analyze step no longer loops forever on the package's own mid-publish plugin-version error. When saropa_lints dogfoods itself, dart analyze reports the plugin dependency as a constraint (^14.0.0) while pubspec.yaml holds the bare version (14.0.0); the mid-publish guard compared the two with string equality, so the caret made it fall through to the interactive [F]/[S] "fix stale cache" prompt — which can never resolve because the package's own analysis_options.yaml has no plugin version: pin to edit, so it cleared the cache, retried, and re-prompted indefinitely. The guard now strips the leading constraint operator before comparing, so the benign mid-publish state is recognized and analyze is treated as passed. Dev-only.
  • The empty-body stub-test guard no longer fails on a deliberately skipped placeholder. A test(..., () {}, skip: '...') documents an un-runnable case (e.g. a Flutter-gated rule the Flutter-less example package cannot exercise) and never executes, so its empty body cannot silently pass — yet the hard zero gate counted it and turned the guard, CI, and the publish audit red. The scanner now excludes any test/testWidgets carrying a skip: argument; a genuine () {} stub with no skip: is still rejected. Dev-only.
  • Regression test pins the Package Vibrancy published-age math against device_calendar 4.3.3. The whole-month age helper is now exported and accepts an injectable reference date, so a fixed-now test asserts the calendar-month, day-of-month-rollback, UTC-neutrality, and clamp behaviors that the old days/365 calculation got wrong. No production behavior change.
  • New publish audit gate: every package imported by shipped code must be a declared dependency. The release audit (STEP 1, before any tag is pushed) now scans lib/ and bin/ for real import/export directives and fails if an imported package is absent from pubspec dependencies. This catches the class of defect that sank v13.12.6 and v13.12.7 (a meta import with no meta dependency): lib/** is in analyzer.exclude for plugin dogfooding, so no dart analyze run inspects these imports, and dart pub publish only rejected them on the post-tag CI job — after the tag was burned. The gate is deterministic and Dart-version-independent, and ignores package: URIs that appear inside rule detection patterns or DartDoc examples.
  • The same gate now runs in CI on every push and pull request. A new scripts/check_dependency_imports.py (shared logic with the release audit) runs in the analyze job, so a missing dependency fails at merge time rather than at release. The pre-existing dart pub publish --dry-run CI step could not be relied on for this: its exit code for a missing dependency is Dart-version-dependent and was treated as a non-fatal warning.
  • New Playwright UX render harness for the editor dashboards. npm run ux renders each dashboard builder's HTML against a light/dark/high-contrast --vscode-* theme shim, runs axe-core, checks horizontal overflow at narrow and wide widths, and captures screenshots — so visual and accessibility regressions are caught outside the VS Code host. Dev-only; generated pages and screenshots are gitignored.
  • Removed the retired inline package-detail card builders from the dashboard source. After single-package detail moved into the docked pane, buildDetailCard and its file/vulnerability/dependency/links section builders were no longer called; deleting them drops dead weight from the webview bundle. The shared Health Score section builder it used was kept (the pane now renders it). No behavior change.
  • Publish no longer crashes after the tag is pushed when the release workflow runs long. The gh run watch step had a hard 5-minute timeout with no handler, so a longer publish workflow raised an uncaught timeout after the version tag was already created. It now waits up to 10 minutes (aligned with run discovery) and, on timeout, prints the Actions monitor URL instead of crashing.
  • The dependency-import publish gate no longer false-blocks on a commented dependencies: header. A dependencies: # comment line was not recognized, so the parser saw zero declared dependencies and reported every shipped import as undeclared. The header now tolerates a trailing comment.
  • get_latest_changelog_version only matches real headings. The version lookup was unanchored and could match a version token in prose or a code fence; it now requires a line-leading ## heading, consistent with the changelog display path.
  • The [Unreleased] publish matchers now only match real headings too. has_unreleased_section and rename_unreleased_to_version used an unanchored regex, so a release note that quotes ## [Unreleased] inside a backtick code-span was read as a leftover Unreleased section after the heading had already been renamed — making the publish step raise "both [Unreleased] and [version] exist" and silently bump to the next patch. Both now anchor to a line-leading heading. Dev-only.
  • The publish prompt now offers a pre-written CHANGELOG release version instead of the stale pubspec patch. The default version was derived only from pubspec.yaml plus whether an ## [Unreleased] section exists, so hand-writing a release section that consumed the [Unreleased] heading (e.g. a ## [14.0.0] major bump) left the prompt suggesting the old pubspec value. It now takes the higher of the pubspec-derived default and the latest ## [X.Y.Z] heading. Dev-only.
  • Project Health size scan stays memory-flat on large projects and ignores a UTF-8 BOM. The NDJSON writer is now flushed with backpressure as the scan streams rows (its buffer no longer grows with file count), and a leading byte-order mark is stripped before line counting so a BOM-prefixed first line is not miscounted as code.
  • New resolved-analyzer rule-test harness. test/support/resolved_rule_harness.dart runs a single lint rule against inline source with full type/element resolution and returns the diagnostics it reports, so a rule's detection can be asserted to fire on a violation and stay silent on compliant code. Previously the rule tests only pinned metadata and counted expect_lint strings, so detection false positives/negatives shipped unverified. Dev-only; used by the new async/parse/gating regression tests.
  • The British-English spelling guard now only scans files inside the repo. The PostToolUse hook fired on edits to files outside the repository (e.g. a global editor config), flagging unrelated content — including reference tables that legitimately list British spellings — as violations. It now skips any path that does not resolve under the repo root; the git pre-commit path and all in-repo edits are unaffected. Dev-only.
  • Every extension command/notification message is now localizable. All showInformationMessage / showWarningMessage / showErrorMessage calls (and their action-button labels) across the extension were routed through l10n() with {token} interpolation instead of hardcoded English or string concatenation — 208 keys under a new notify.* namespace in en.json. English output is unchanged; the translated locale catalogs are filled by the separate translation pass. Quick-pick / input-box / progress titles and webview HTML are not part of this pass.
  • Closed the last three extension translation gaps by hand. Filipino Cross-project drift, Indonesian info, and the Russian rule-pack-review error toast had come back as untranslated English from the MT pass; each now has a curated dictionary entry and locale-catalog value, bringing every locale to full coverage.
  • Translation generator auto-corrects when launched on a free-threaded Python. generate_translations.py relaunches under the standard python.exe beside a free-threaded build (via a blocking subprocess, so the console isn't shared and the interactive menu's stdin stays intact), because the shared NLLB runtime (numpy/ctranslate2) is compiled for the standard ABI and fails to import under python3.14t; if no standard build is found it errors with the exact command instead of a deep numpy ImportError. Dev-only.
  • Interactive translation menu previews the gap state first and reads cleaner. An interactive launch of generate_locales.py now runs the read-only audit before the mode menu so the choice is informed by what is actually missing, adds blank lines around the menu (the block was crushed against the preceding output), and states the Enter default in the prompt (default 1). Dev-only.
  • Ctrl-C on the translation generator now stops promptly instead of finishing the whole in-flight locale. The cooperative-cancel flag was honored by the prefetch loop but not by the per-leaf mapping pass, so after a stop request the generator silently made live NLLB/Google calls for every remaining string in the current locale before exiting; the shared single-string path now also short-circuits on cancel, serving already-cached work and leaving the rest as a gap the next run resumes. Dev-only.
  • Closed the last two extension translation gaps by hand. The German dependency-sort detail ({detail} in {sections}) and the Persian command-notification brand prefix (Saropa Lints: {message}) came back identical to English from the MT pass — German "in" is spelled the same and the Persian string is brand-plus-placeholder with no translatable words — so each now has a curated dictionary passthrough, bringing every active locale back to full coverage. Dev-only.
  • Closed the Italian and Hindi translation gaps by hand. The Italian dependency-sort detail ({detail} in {sections}) and the Hindi command-notification brand prefix (Saropa Lints: {message}) returned identical to English from the MT pass — Italian "in" is spelled the same and the Hindi string is brand-plus-placeholder with no translatable words — so each now has a curated dictionary passthrough, keeping the coverage gate honest. Dev-only.
  • Bumped the extension build's esbuild devDependency to 0.28.1 to clear GHSA-gv7w-rqvm-qjhr. The advisory's RCE only affects esbuild's Deno install path via a malicious NPM_CONFIG_REGISTRY; the extension bundles through the Node path (which already verifies binary hashes) and esbuild never ships to users, so there was no runtime exposure. Dev-only.
  • Resolved the shell-quote critical advisory (GHSA-w7jw-789q-3m8p) in the extension's dev dependency tree. A transitive dev-only dependency was bumped via npm audit fix; it is build/test tooling and never ships to users. Two remaining low-severity diff-via-mocha advisories have no non-prerelease fix and stay until mocha 12 is stable. Dev-only.

13.13.0 #

Adds crash, performance, and contract rules for seventeen more packages, each active only in files that import it. Introduces version-gated migration packs that flag code which will break on a major-version upgrade, so you can prepare before bumping. The VS Code Findings Dashboard now mirrors the live Problems panel, never showing a stale grade while real warnings sit unaddressed. No action required.

Added #

  • Three receive_sharing_intent rules. rsi_missing_initial_media (Essential) flags a file that subscribes to getMediaStream() (warm-share) but never calls getInitialMedia() — silently dropping every cold-start share; rsi_missing_reset_after_initial_media flags getInitialMedia() in a class that never calls reset() (stale intent re-delivered on resume); rsi_unfiltered_shared_media_type (INFO) flags a handler that reads SharedMediaFile fields without ever checking .type. Run only in files that import receive_sharing_intent. No action required.

  • Six sign_in_with_apple rules. Flags getAppleIDCredential() not wrapped in a try/catch (every failure, including user-cancel, throws), a catch that never handles AuthorizationErrorCode.canceled, a file that never checks isAvailable(), identityToken / givenName / familyName / email (all nullable, name+email only returned on first sign-in) assigned to non-nullable String, and a discarded getCredentialState() result. The token rule maps to OWASP M3. Run only in files that import sign_in_with_apple. No action required.

  • Five lottie rules. Flags a Lottie.* factory with a controller: but no onLoaded: (animation frozen at frame 0), Lottie.network with no errorBuilder: (blank widget on load failure) or no backgroundLoading: true (JSON parsed on the UI thread), FrameRate.max with no renderCache: (quadrupled repaints on 120 Hz), and RenderCache.raster (a documented memory-pressure risk). The receiver is resolved to the Lottie type, so Image.network and friends never trip. Run only in files that import lottie. No action required.

  • Six flutter_animate rules. Flags an unconditional controller.repeat() in onPlay (an infinite off-screen animation that burns battery), Animate.restartOnHotReload = true shipped without a kDebugMode guard (ERROR), an .animate()/Animate(...) list element with no key: (animation restarts on rebuild), AnimateList(children: []) / [].animate() (dead code), a literal target: (state-driven param hard-coded), and autoPlay: false with no controller/adapter/target driver (animation never starts). Run only in files that import flutter_animate. No action required.

  • Seven awesome_notifications rules. Flags an instance (non-static) or wrong-parameter-type setListeners handler (ERROR — both fail at runtime), a static handler missing @pragma('vm:entry-point') (tree-shaken in release), a NotificationContent(channelKey:) literal not declared in the same file's initialize() channel list (notification silently discarded), createNotification() with no isNotificationAllowed() guard, a negative notification id (with a quick fix — negatives are silently randomized, breaking cancel(id)), and notification calls ordered before setListeners(). Run only in files that import awesome_notifications. No action required.

  • share_plus migration + correctness rules. prefer_shareplus_instance (gated share_plus >= 11.0.0, with a quick fix) rewrites the deprecated static Share.share/shareUri/shareXFiles to SharePlus.instance.share(ShareParams(...)), preserving sharePositionOrigin. Four always-on rules flag a ShareParams with no sharePositionOrigin (iPad crash), a discarded ShareResult, all-empty content fields (ERROR), and a uri+text conflict (ERROR). No action required.

  • sensors_plus migration + best-practice rules. prefer_sensors_event_stream (gated sensors_plus >= 4.0.0, with a quick fix) rewrites the deprecated accelerometerEvents-style getters to the accelerometerEventStream() functions. Three always-on rules flag a missing samplingPeriod, SensorInterval.fastestInterval (max battery drain), and a .listen() with no onError:. No action required.

  • flutter_svg migration + correctness rules. prefer_svg_color_filter (gated flutter_svg >= 2.0.0, with a quick fix) rewrites the deprecated color:/colorBlendMode: on SvgPicture.* to colorFilter: ColorFilter.mode(...); the receiver is resolved to SvgPicture so Icon/Container color: never trips. Four always-on rules flag SvgPicture.network/.string with no errorBuilder:, network with no placeholderBuilder:, and any SvgPicture with no semanticsLabel/excludeFromSemantics. No action required.

  • Four file_picker deprecation rules. file_picker_deprecated_allow_compression (gated file_picker >= 10.0.0, with a quick fix mapping allowCompression: truecompressionQuality: 75); file_picker_deprecated_with_data/with_read_stream/allow_multiple (gated file_picker >= 12.0.0). All relocated into the gated packs so a project on the old major never sees them. No action required.

  • connectivity_plus pre-upgrade migration rule. avoid_pre_v6_single_connectivity_result (gated connectivity_plus < 6.0.0, with a partial quick fix == ConnectivityResult.x.contains(...)) flags single-value handling that breaks on the v6 List<ConnectivityResult> change; connectivity_satellite_missing flags an if-else chain over ConnectivityResult missing the v7.1 satellite case. No action required on v6+.

  • google_sign_in migration + v7-usage rules. avoid_pre_v7_google_sign_in (gated google_sign_in < 7.0.0) flags the removed GoogleSignIn() constructor / signIn(). Five usage rules (gated google_sign_in >= 7.0.0) flag authenticate() with no try/catch, no supportsAuthenticate() guard, .accessToken read off an account (ERROR — null in v7), a catch that ignores canceled, and authenticate() before initialize(). No action required.

  • Four local_auth 3.0 pre-upgrade migration rules. Gated local_auth < 3.0.0: local_auth_deprecated_options_class flags the removed AuthenticationOptions, local_auth_use_error_dialogs_removed flags its removed useErrorDialogs (ERROR), local_auth_sticky_auth_renamed renames stickyAuth:persistAcrossBackgrounding: (quick fix), and local_auth_platform_exception_catch rewrites a PlatformException catch to LocalAuthException (quick fix). No action required on v3+.

  • Three app_links 6.0 pre-upgrade migration rules. Gated app_links < 6.0.0: app_links_use_get_initial_link and app_links_use_get_latest_link rename the removed getInitialAppLink()/getLatestAppLink() to getInitialLink()/getLatestLink(), and app_links_use_uri_link_stream renames the removed allUriLinkStream/allStringLinkStream getters to uriLinkStream/stringLinkStream — each with a quick fix, relocated into the gated app_links_6 pack so a project on v6+ never sees them. No action required on v6+.

  • Eight geocoding rules. geocoding_unchecked_first (ERROR) flags .first/.last on a geocoding result with no emptiness guard (the common StateError crash); geocoding_missing_exception_handler flags a lookup not in a try/catch; geocoding_prefer_no_result_found_catch (INFO) flags catching PlatformException but not NoResultFoundException; geocoding_locale_set_before_call (INFO) and geocoding_concurrent_locale_race cover the v3 setLocaleIdentifier API and its concurrency race; geocoding_missing_is_present_check (INFO) flags a lookup with no isPresent() gate; geocoding_call_in_text_field_listener flags per-keystroke geocoding with no debounce; geocoding_deprecated_locale_param (ERROR) flags the removed localeIdentifier: argument. Run only in files that import geocoding; the crash + migration rules are Recommended, the rest Comprehensive. No action required.

  • Five quick_actions rules for the app-shortcut initialization contract and ShortcutItem fields. quick_actions_set_before_initialize and quick_actions_missing_initialize flag shortcuts that register before, or without, the initialize(handler) call that opens the cold-start callback channel — the common cause of a shortcut that opens the app but takes no action. quick_actions_empty_shortcut_type (with a quick fix), quick_actions_empty_localized_title, and quick_actions_flutter_asset_icon flag invalid ShortcutItem arguments that render a dead, blank, or icon-less shortcut. All five run only in files that import quick_actions and live in the Professional tier. No action required.

  • Five in_app_review rules for the review-prompt contract. in_app_review_missing_availability_check flags requestReview() with no isAvailable() guard; in_app_review_button_callback_request and in_app_review_request_in_init_state flag prompting from a button tap or in initState (both burn the once-a-year quota with no result, against store guidelines); in_app_review_missing_store_listing_fallback (INFO) notes a rate button with no openStoreListing() escape hatch; in_app_review_ios_store_listing_missing_app_id flags openStoreListing() without an appStoreId on projects that target iOS or macOS. All five run only in files that import in_app_review and live in the Comprehensive tier. No action required.

  • Five local_auth biometric-auth rules. local_auth_unchecked_result flags a discarded authenticate() result (cancel reads as success); local_auth_missing_capability_check (INFO) flags a file with no canCheckBiometrics/isDeviceSupported() guard; local_auth_unhandled_exception flags an authenticate() not wrapped in a try/catch covering LocalAuthException (14 failure codes); local_auth_missing_lockout_handling (INFO) flags a catch that ignores the lockout codes; local_auth_biometric_only_sensitive (Pedantic, with a quick fix) flags a sensitive-context call missing biometricOnly: true. The first three map to OWASP M3 (Insecure Authentication). The 3.0 migration rules (AuthenticationOptions removal, stickyAuth rename, PlatformException catch) are tracked separately. No action required.

  • Six file_picker correctness rules. file_picker_unchecked_null_result flags using a FilePickerResult? member with no null check (cancel returns null); file_picker_path_on_web (experimental) flags force-unwrapping PlatformFile.path (null on web); file_picker_custom_type_missing_extensions (ERROR) and file_picker_extensions_without_custom_type flag the FileType.custom / allowedExtensions contract; file_picker_extension_with_dot flags (and fixes) a leading-dot extension; file_picker_with_data_large_files flags withData: true with allowMultiple: true (OOM risk). All run only in files that import file_picker and live in the Comprehensive tier. The version-gated withData/withReadStream/allowMultiple/allowCompression deprecation rules are tracked separately. No action required.

  • Seven device_calendar rules. device_calendar_missing_permission_check (INFO) flags data operations in a file with no hasPermissions/requestPermissions; device_calendar_unchecked_result flags a discarded awaited Result; device_calendar_result_data_before_success_check flags reading Result.data with no isSuccess guard; device_calendar_retrieve_events_empty_params and device_calendar_retrieve_events_missing_end_date flag invalid RetrieveEventsParams; device_calendar_event_missing_calendar_id (ERROR) flags an Event with no calendarId passed to createOrUpdateEvent; device_calendar_event_utc_timezone flags TZDateTime.utc on an event (the Android wrong-local-time bug). All seven run only in files that import device_calendar and live in the Comprehensive tier. No action required.

  • Six google_maps_flutter rules. Flags per-frame Set<Marker/Polyline/…> and BitmapDescriptor rebuilds plus animateCamera/moveCamera calls inside build() (map jank and queued platform calls), the deprecated cloudMapId: argument (with a rename fix to mapId:) and setMapStyle API, and unguarded info-window calls that throw UnknownMapObjectIDError since 2.0. All run only in files that import google_maps_flutter; the crash and safe-migration rules are Recommended, the rest Professional/Comprehensive. No action required.

  • Six audioplayers rules for audio-specific traps the generic media rules miss. Flags onPositionChanged/onDurationChanged/onPlayerComplete listeners and seek() on a PlayerMode.lowLatency player (events that never fire), an onPlayerComplete listener under ReleaseMode.loop, an undisposed AudioPool field, a UrlSource built from a bundled assets/ path (with a fix to AssetSource), and a setVolume/play(volume:) literal above 1.0. All run only in files that import audioplayers. No action required.

  • Six flutter_map rules. flutter_map_missing_user_agent (Recommended) flags a TileLayer with no userAgentPackageName (OpenStreetMap blocks unidentified traffic, so tiles silently fail in production); the rest flag the v8 tileSize/labelPlacement and v6 MapOptions center/zoom deprecations (two with rename fixes), a NetworkTileProvider fallbackUrl that disables the in-memory cache, and a TileLayer with no errorTileCallback. All run only in files that import flutter_map. No action required.

  • Five youtube_player_flutter rules for the v10 iframe rewrite. Flags a YoutubePlayerController field never close()d (its cleanup is close(), not dispose(), so generic disposal rules miss it), convertUrlToId(...) used without a null check, the deprecated YoutubePlayerScaffold wrapper, an unmuted autoPlay (blocked by browser policy), and autoFullScreen with no orientation/pop guard. All run only in files that import youtube_player_flutter. No action required.

  • Five new image_picker rules covering gaps in the existing coverage. image_picker_missing_retrieve_lost_data flags a file that picks media but never calls retrieveLostData() (the Android process-death recovery that silently drops the user's selection); image_picker_invalid_image_quality (ERROR, with a quick fix that clamps to 0–100) flags an imageQuality literal outside the asserted range (a confirmed iOS 16+ crash); image_picker_camera_source_without_support_check flags ImageSource.camera with no supportsImageSource/platform guard (it throws on web/desktop); image_picker_lost_data_empty_check_missing flags reading a LostDataResponse with no isEmpty guard; image_picker_multi_result_unchecked_empty (ERROR) flags [index] access on a pickMultiImage/pickMultipleMedia result with no emptiness guard (cancel returns an empty list, so it throws RangeError). All five run only in files that import image_picker and complement the package's existing result-handling, size, and source rules. No action required.

  • Six home_widget rules for the home-screen-widget contract. home_widget_callback_missing_pragma and home_widget_callback_not_top_level flag interactivity callbacks that are tree-shaken in release builds (missing @pragma('vm:entry-point')) or can't be serialized (closures / instance methods); home_widget_save_without_update flags saveWidgetData with no updateWidget in the same member (the widget shows stale data); home_widget_update_no_name flags updateWidget() with no target name (a silent no-op); home_widget_ios_missing_app_group flags saveWidgetData/getWidgetData in a class that never sets the iOS App Group; home_widget_widget_clicked_without_initial_launch (INFO) flags listening to widgetClicked with no cold-start initiallyLaunchedFromHomeWidget() check. All six run only in files that import home_widget and live in the Comprehensive tier. No action required.

  • One webview_flutter pre-upgrade migration rule. avoid_pre_v4_webview_widget (Comprehensive, WARNING) flags construction of the removed WebView widget in files importing webview_flutter, warning that the widget was deleted in v4.0.0 and the replacement is WebViewController + WebViewWidget(controller:). Report-only — the structural rewrite cannot be automated. Gated to webview_flutter < 4.0.0 via the webview_flutter_4 pre-upgrade readiness pack. No action required on v4+.

Fixed #

  • require_timezone_display no longer flags seconds-only formats or .pattern-only introspection. A format whose only time field is seconds (DateFormat('ss'), DateFormat('s')) can never be misread across time zones, because timezone offsets are always whole-minute or coarser — so a timezone indicator there is meaningless and is no longer required. A DateFormat built solely to read its .pattern string (the DateFormat.jm(locale).pattern locale-hour-detection idiom) is also skipped, since nothing is ever displayed. Formats that include hours or minutes (e.g. DateFormat('mm:ss')) still report. Remove any // ignore: you added for these. No action required otherwise.
  • require_cache_expiration no longer flags a size-bounded cache that has no TTL. A capacity- or maxSize-bounded LRU/LFU cache caps its own memory by eviction, so the rule's out-of-memory argument does not apply and a deliberately TTL-less bounded cache is a valid design; unbounded growth remains avoid_unbounded_cache_growth's concern. The rule also now detects Map storage from field declarations instead of a whole-source scan, so a toMap() return type is no longer mistaken for a cache. Remove any // ignore: you added for these. No action required otherwise.
  • Beta and deprecated rules are now excluded by default from configs written by the VS Code extension and CI, matching the interactive init. The headless config writer (used when the extension or CI generates analysis_options.yaml) applied tier, platform, and package filters but skipped the lifecycle filter, so a beta or deprecated rule sitting in a selected tier was enabled there while init excluded it. Both paths now share one filter: beta/deprecated rules stay off unless you explicitly enable them in analysis_options_custom.yaml RULE OVERRIDES. If you relied on a beta rule being auto-enabled by the extension, add it to your overrides. Otherwise no action required.

Added (Extension) #

  • New consolidated dashboard — "Saropa Lints: Open Dashboard". A live, rule-grouped view of every linter finding: a holistic grade gauge, findings collapsed into per-rule rows ranked by severity, and lazy-expand occurrence lists that jump to source on click. It reads diagnostics live (zero analysis, so it stays in sync with the Problems panel) and updates as you edit. Open it from the Command Palette. No action required.

Changed (Extension) #

  • Applicable rule-pack suggestions now arrive as one notification on project open instead of several competing toasts — including the version-gated upgrade packs that previously fired their own per-package prompt — routed through a single "Review" action to the Suggestions view (and its always-present activity-bar badge), so two packs no longer push each other off-screen before you can read them. No action required.
  • The UI-language picker now shows translation coverage. Languages that are not fully translated are badged with their measured percentage (e.g. "4% translated — rest shows in English") in the language quick-pick, so picking one is an informed choice instead of landing on a near-English UI under a non-English label. The percentages are generated by the i18n audit, so the badge always matches measured reality. No action required.
  • The Findings Dashboard now reads live analyzer findings instead of a saved report, so it stays in sync with the Problems panel — no more stale "0 findings, grade A" while warnings sit there — and refreshes as you edit with no manual "Run analysis" step. No action required.
  • The dashboard is now holistic, listing findings from built-in Dart lints and other custom_lint plugins alongside Saropa rules in one combined view instead of Saropa rules alone. No action required.
  • The status-bar score and the Issues list now read live analyzer findings like the dashboard, so they stay in sync with the Problems panel (no stale grade or finding count between runs), refresh automatically as you edit, and now include findings from built-in Dart lints and other custom_lint plugins. No action required.
  • The editor code-lens count and inline end-of-line annotations now read live analyzer findings too, so the "N violations" lens above a file and the annotation text alongside each line match the squiggles exactly and update as you edit, instead of reflecting the last saved report. No action required.
  • The Issues panel's "filter by rule type/status" and security-hotspot review now work off live findings. A rule-details catalog bundled with the extension (covering every rule's type, lifecycle status, and security-review flag) supplies the metadata that live analyzer findings don't carry, so these two actions stay in sync with the Problems panel instead of needing a saved report first. No action required.

Fixed (Extension) #

  • Filled four extension strings that were shipping English in their locale — the pub.dev 30-day download count (Hindi), the transitive-dependencies tooltip (Filipino), and the size label (Filipino, Swahili). No action required.
  • Corrected the pub.dev likes count in German, Arabic, and Persian, where machine translation had rendered the social-media noun "likes" as the verb "to like". No action required.
  • Cleaned up garbled machine-translation in the package-dashboard tooltips and labels across all 24 languages. The dependency-list tooltips, transitive-dependency headers, the size label, and the 30-day download count had picked up hallucinated trailing phrases (a dependency name followed by "person with a disability", "physiotherapy", or "blood circulation") and mistranslated technical terms (Korean "geometric progression", Russian "indents" for "transitive dependencies"); they now read as clean, correct labels in every language, with the dependency bullet and size label kept as a curated passthrough so the coverage gate stays honest. No action required.
  • Removed a duplicate "Open Findings Dashboard" row from the sidebar Actions panel. The same action already appears as "Findings Dashboard" in the Dashboards section, so the Actions-panel copy was redundant. No action required.
  • The "Enable upgrade lints" nudge no longer fails with "could not write analysis_options.yaml (rule_packs)" on configs that omit the plugin version: pin. The writer anchored the new rule_packs block only on a version: line directly under saropa_lints:; projects that load the plugin from source (no version pin) had no anchor, so the write silently returned false and the toast errored. It now falls back to inserting the block as the first child of the saropa_lints: mapping. No action required.
  • Fixed unreadable folder and file labels on the Saropa Project Map size treemap. Folder headings rendered light-on-light (invisible) in dark mode and the darkest tiles showed dark-on-dark file names, because label colors keyed off the page's light/dark mode while the tile fills always use the same orange heat ramp. Each label now picks black or white text from its own tile color, so headings stay legible on every tile. No action required.

Removed (Extension) #

  • Removed the "show other analyzer findings" and "show analyzer TODOs" Findings Dashboard toggles (both settings and their commands). Now that the dashboard is holistic those diagnostics appear directly in the main findings list, so the separate opt-in pills were redundant. If you had either setting enabled you can delete it from your settings.json; otherwise no action required.
Maintenance
  • The extension i18n audit and NLLB-fallback reports now write to datetime-stamped, day-bucketed paths (extension/reports/<YYYYMMDD>/<YYYYMMDD_HHMMSS>_i18n_translation_audit.md) instead of a single fixed file, so successive audit runs no longer overwrite each other and each run stays a durable record. Build tooling only (excluded from the .vsix); no behavior change for users.
  • Rewrote the archived finish reports under plans/ into third-person engineering-record voice, removing AI-session narration (chat-quoting Trigger openers, "reviewed by another AI" lines, first-person/session deixis) from 58 internal documents while preserving every technical fact. Documentation housekeeping only; not shipped to pub.dev.
  • Added headless execution coverage for the consolidated dashboard's webview client: a new test evals the template-literal client against a minimal recording-DOM harness (no new dependency) and drives its load, model-patch, and occurrences-render paths, so a syntax error or a regex literal mangled by template-literal escaping now fails CI instead of shipping as a blank dashboard. Test harness only; no runtime change.
  • Wrapped the consolidated dashboard's debounced model push in an error boundary so a model-build throw is logged and skipped instead of silently killing the live-refresh loop. Defensive hardening; no change on the success path.
  • Decomposed the editor Findings dashboard's HTML builder (violationsDashboardHtml.ts, ~2155 lines) into a thin composer plus focused section modules — shared primitives, the embedded client script, and per-area builders for the hero/KPI/toolbar chrome, the tables, and the aside panels. Behavior-preserving (rendered output unchanged, dashboard test green); the section builders become the reusable building blocks for the planned single central dashboard. Internal refactor; no user-facing change.
  • Moved American-English enforcement from publish-time-only to write-time and commit-time: a git pre-commit hook and a Claude editor hook now scan touched files with the existing spelling checker and block before British spellings can land, and the publish gate no longer offers an "ignore and ship" choice. Corrected the British spellings in rule docstrings and lint messages that had slipped through under the old bypassable gate. Developer tooling only; no change to published rule behavior.
  • Widened the American-English spelling dictionary to cover 22 previously-missed British words (now mapped to dialog, realize, analyze, labeled, aluminum, and others), excluding the ambiguous plurals of analysis/paralysis so the correct American nouns are not mis-flagged. Developer tooling only; no change to published rule behavior.

13.12.7 #

Sharpens roughly thirty leak, disposal, security, and package rules so they stop flagging code that is already correct, ahead of grading some of them as build-breaking errors. Resources cleaned up in a helper or handed to a caller, controllers owned by a parent widget, encrypted SharedPreferences keys, and Drift queries that do carry a where are no longer reported. Names are matched on whole-word and resolved-type boundaries instead of substrings, so pin no longer matches shopping and ui.ImageFilter is no longer mistaken for a disposable image. No action required.

Added #

  • New opt-in rule prefer_us_english_spelling. Flags British spellings in comments and prose string literals so a project can standardize on American English, sharing the same word list as the package's own spelling audit. It is in the stylistic (opt-in) set and off by default — enable it in analysis_options.yaml; there is no quick fix yet.

Changed #

  • require_platform_channel_cleanup upgraded from warning to error. After interprocedural cleanup tracking plus an AST-based setup check (a string literal that merely mentions setMethodCallHandler no longer triggers it), a MethodChannel/EventChannel handler left active past dispose() — which fires callbacks on an unmounted widget and crashes with a setState error — now fails dart analyze. require_websocket_close and the per-method resource rules (require_file_close_in_finally, require_http_client_close, require_native_resource_cleanup) were re-evaluated and stay warnings: each can still false-positive on cleanup that escapes the analyzable class (a parent-owned socket, or a handle passed to a function in another file), which is acceptable for a warning but not for a build-breaking error.
  • Ten high-confidence rules upgraded from warning to error. After per-rule false-positive hardening, these rules now fail dart analyze instead of warning, because each fires only on a genuinely broken shape (a leak with no cleanup anywhere, a Drift delete/update with no where, a missing iOS permission verified against the actual Info.plist, a non-JSON-encodable toJson value, a sub-333ms seizure-risk flash): avoid_websocket_memory_leak, require_dispose_implementation, prefer_dispose_before_new_instance, avoid_stream_subscription_in_field, avoid_not_encodable_in_to_json, avoid_drift_update_without_where, require_ios_permission_description, require_ios_face_id_usage_description, avoid_flashing_content, avoid_path_traversal. If one fires on your code, it has found a real defect — fix it, or downgrade the single rule in your analysis_options.yaml if your project is the exception.

Changed (Extension) #

  • The Findings dashboard "Updated" stamp is now a live, clickable refresh. Its status-line pill re-ticks its own age (just now5m ago) and re-reads the analyzer's current findings when clicked or activated by keyboard, so a dashboard left open in a background tab no longer shows a frozen just now from its last paint. No action required.

Removed #

  • prefer_returning_shorthands removed — it duplicated prefer_arrow_functions. Both flagged a block body holding a single return, so the stylistic tier double-reported on the same line; the convert-to-=> quick fix moved onto prefer_arrow_functions (which additionally covers lambdas). If you enabled the rule by name in analysis_options.yaml, switch to prefer_arrow_functions.

Fixed #

  • Five resource-cleanup rules now follow cleanup into helper methods. A close/free/teardown performed in a method that the owner delegates to (dispose()_teardown()_channel.sink.close(), or save()_writeAndClose(sink)) is recognized instead of flagged, via new interprocedural (cross-method) cleanup tracking that walks same-class method calls: require_websocket_close, require_platform_channel_cleanup, require_file_close_in_finally, require_http_client_close, require_native_resource_cleanup. This also makes them catch a real leak the previous "any private call counts as cleanup" heuristic wrongly suppressed.
  • require_websocket_close skips parent-supplied sockets. A WebSocketChannel/WebSocket field assigned from widget.* (owned and closed by the parent) is no longer flagged for a missing local close.
  • Seven resource-cleanup rules no longer false-positive on ownership transfer. require_file_close_in_finally, require_http_client_close, require_native_resource_cleanup, and require_database_close now skip a resource assigned to a field or returned to the caller (closed elsewhere), require_native_resource_cleanup recognizes Arena/using auto-free scopes and no longer demands a free() on a borrowed Pointer.fromAddress, and require_platform_channel_cleanup/require_isolate_kill/require_websocket_close recognize teardown delegated to a helper and ignore matching text in string literals. require_platform_channel_cleanup also no longer re-flags a class that cleans up via removeMethodCallHandler.
  • Disposal and lifecycle rules respect ownership. avoid_websocket_memory_leak, require_stream_subscription_cancel, require_dispose_implementation, and require_field_dispose now skip resources supplied by a parent (widget.controller) and recognize cleanup performed in any method, not just dispose(); prefer_dispose_before_new_instance no longer flags an assignment guarded by a null check.
  • Name and type matching tightened across security and storage rules. Key matching in prefer_encrypted_prefs/avoid_auth_state_in_prefs is now whole-word (pin no longer matches shopping, spinner, mapping), require_secure_storage skips objects that are not actually SharedPreferences, avoid_deprecated_crypto_algorithms no longer treats the des abbreviation (description) as DES, and require_secure_password_field inspects obscureText as a literal rather than matching nested children.
  • Memory and path rules use resolved types and word boundaries. require_image_disposal matches exactly ui.Image (not ui.ImageFilter/Provider/Descriptor), avoid_expando_circular_references requires a real Expando and a whole-token key, and avoid_path_traversal/require_file_path_sanitization match the tainting parameter as a whole identifier.
  • Drift, WebView, and animation detection narrowed to real sinks. avoid_drift_update_without_where walks the query's own receiver chain so an unrelated .write()/.go() no longer trips it and a query carrying where is cleared; prefer_html_escape targets only the HTML argument of a WebView sink; avoid_flashing_content requires a literal repeat(reverse: true) with a sub-333ms duration.
  • prefer_no_commented_out_code no longer flags wrapped prose. A prose comment whose final wrapped line is a single word ending a sentence (// result.), or a line that names an API method mid-sentence (// base64Url.decode rejects the token.), is no longer mistaken for commented-out code. The rule now judges each contiguous // run as one block rather than line by line, so the surrounding prose is taken into account; a genuine commented-out statement sitting under a prose comment is still flagged. No action required.

Fixed (Extension) #

  • The "Review" button on the rule-pack suggestion notification now opens the Config Dashboard. Clicking Review previously tried to focus a sidebar tree, which produced no visible change when that view was hidden or collapsed (or its sidebar was already open) and surfaced no error — so the button appeared dead. It now opens the rule-pack Config Dashboard in an editor tab, where each applicable pack can be reviewed and toggled, and any failure is shown instead of swallowed. No action required.
  • Dashboard labels stranded in English are now localized in six languages. The review status "not applicable", the Persian "version" section header, the French links heading, and the documentation quality badge now render in the active display language for German, Spanish, Persian, Bengali, Filipino, and French instead of showing English; acronyms and cognates that are correct as-is (WASM, PR, Repository) are kept English on purpose. No action required.
Maintenance
  • Declared meta as a direct dependency. Eight rule files import package:meta/meta.dart; dart pub publish errors (exit 65) when an imported package is not in dependencies, which blocked the pub.dev release. Constraint is ^1.18.0 to stay compatible with Flutter stable's exact 1.18.0 meta pin (a higher floor would make the package unresolvable for Flutter consumers).

13.12.5 #

Adds one-click quick fixes for eight more lint rules, so common simplifications can be applied straight from the IDE lightbulb instead of by hand. An if/else (or if plus a following return) that returns true in one branch and false in the other collapses to a direct return of the condition, a nested if with no else on either level merges into a single combined condition, an explicit null-check-then-call becomes the null-aware ?. form, and a class that holds only static members is marked abstract final so it can no longer be instantiated. Redundant arguments and a redundant nullable ? are now removable in one click too. A severity recalibration moves 46 rules that flag preferences, performance, robustness gaps, or deployment-config issues (not broken or crashing code) from error down to warning, so they no longer fail a strict build; rules that mark genuine compile errors, runtime crashes, data corruption, or security holes keep error severity. No action required. log

Added #

  • avoid_unnecessary_if and prefer_returning_condition gain a quick fix that returns the condition directly. if (c) return true; return false; becomes return c; and the if/else form becomes return c; / return !(c);; the condition is parenthesized when negated so operator precedence stays correct. No action required.
  • avoid_collapsible_if gains a quick fix that merges the nested if into its parent. if (a) { if (b) { … } } becomes if ((a) && (b)) { … }, with both conditions parenthesized to preserve precedence. No action required.
  • prefer_null_aware_method_calls gains a quick fix that rewrites the guard with ?.. if (x != null) x.foo(); and x != null ? x.foo() : null both become x?.foo(), reusing the original receiver and arguments verbatim. No action required.
  • avoid_classes_with_only_static_members gains a quick fix that adds abstract final modifiers. This makes a static-only utility class non-instantiable, matching the existing prefer_abstract_final_static_class fix. No action required.
  • avoid_icon_size_override and avoid_riverpod_string_provider_name gain a quick fix that removes the flagged named argument. The size: argument on Icon and the name: argument on a provider are deleted together with their comma so the remaining arguments stay valid. No action required.
  • avoid_nullable_parameters_with_default_values gains a quick fix that removes the redundant ?. A parameter with a non-null default does not need a nullable type, so int? x = 0 becomes int x = 0. No action required.
  • New riverpod_2 rule pack gates the Notifier-migration rule on Riverpod 2.x. prefer_notifier_over_state recommends migrating StateProvider to NotifierProvider, an API that only exists in Riverpod 2.0+. The new pack enables that rule only when pubspec.lock resolves riverpod >= 2.0.0, so a Riverpod 1.x project is never told to adopt an API it does not have. Enable it with rule_packs: { enabled: [riverpod_2] } (the VS Code Rule Packs view lists it when Riverpod 2.x is detected). No action required.
  • Four more version-gated migration rule packs: dio_5, bloc_8, riverpod_3, go_router_6. Each enables a new rule only when pubspec.lock resolves the package to the gated version, so a project on an older major never sees a recommendation it cannot follow: avoid_dio_error (dio >= 5.0.0) flags the removed DioError type with a quick fix to DioException; avoid_bloc_map_event_to_state (bloc >= 8.0.0) flags the removed mapEventToState override; avoid_riverpod_state_notifier (riverpod >= 3.0.0) flags the legacy StateNotifier / StateNotifierProvider; avoid_go_router_legacy_redirect (go_router >= 6.0.0) flags the pre-6.0 single-argument redirect callback. Enable any with rule_packs: { enabled: [...] } (or accept the new VS Code upgrade-pack offer). No action required.

Changed #

  • Severity recalibration: 46 rules downgraded from error to warning. A rule-by-rule audit against a three-level model (error = broken/crash/exploit/MUST-fix; warning = should-fix; info = FYI) found 46 rules whose error severity over-stated them — Riverpod/Bloc preferences and should-fix patterns, performance rules (avoid_get_find_in_build, require_vsync_mixin, avoid_provider_of_in_build), robustness-fallback rules (require_error_boundary, require_unknown_route_handler, require_go_router_fallback_route, require_deep_link_fallback), deployment-config heuristics (require_android_manifest_entries, require_ios_info_plist_entries, require_macos_entitlements, require_firestore_index), accessibility (avoid_hidden_interactive), and test-quality (require_test_widget_pump, avoid_print_in_release). They now report as warnings and no longer fail a strict build. Rules that mark genuine compile errors (duplicate_constructor_declarations, uri_does_not_exist), runtime crashes (avoid_setstate_in_build, avoid_recursive_widget_calls, require_provider_scope, avoid_circular_redirects), data corruption/loss (avoid_isar_enum_field, avoid_isar_schema_breaking_changes), or security holes (require_https_only, require_route_guards) keep error severity.
  • prefer_notifier_over_state moved out of the base riverpod rule pack into the new gated riverpod_2 pack. If you enable rule packs with rule_packs: { enabled: [riverpod] } and want this rule, add riverpod_2 to the list. The move keeps the rule from reaching Riverpod 1.x projects, where its recommended NotifierProvider target does not exist. Tier-based configurations are unaffected.

Fixed #

  • prefer_correct_callback_field_name no longer flags non-callback function-typed fields and parameters. The on prefix it asks for is for event handlers, but the rule treated every function type as a callback, so value providers (DateTime Function()? now), action thunks (void Function() start), and builder/validator members were wrongly told to rename. It now fires only on genuine callback typedefs (VoidCallback, ValueChanged, ValueSetter, anything ending in Callback) and skips names whose role is builder/validator/getter/setter/factory/provider/predicate/comparator/selector. Remove any // ignore: you added for these. No action required otherwise.
  • prefer_trailing_comma_always no longer demands a comma after a block-formatted collection-literal argument. When a call's last argument is a multi-line list, set, or map literal (foo(<int>{ ... })), dart format hugs the bracket to the call paren and adds no trailing comma — so the rule's request was a false positive that dart format actively reverses. The argument-list exemption already covered a trailing function-expression callback; it now covers collection literals the same way, matching the SDK require_trailing_commas block-formatted-last-argument exemption. Two scalar arguments with no block still require the comma. Remove any // ignore: you added for these.
  • prefer_correct_handler_name no longer flags boolean state getters and predicates that end in a handler suffix. The rule asks event handlers to start with on/handle, but it matched any method whose name ended in a past-tense suffix (Closed, Changed, Loaded, …) — so a state query like bool get isClosed was wrongly told to become onClosed. It now skips getters entirely (a getter returns state, never handles an event) and skips boolean-predicate names prefixed with is/has/can/should/will/did, where the suffix is an adjective, not an event. Genuine handlers (void itemDeleted()) still lint. Remove any // ignore: you added for these.
  • prefer_reusing_assigned_local no longer flags a receiver-less call reused into a later local. A bare this-call like a recursive-descent parser's _term() advances a cursor and returns a different value each call, so the second call must run — but the rule treated camelCase calls as pure reads and told you to reuse the first result, which would skip the parse. It now treats any receiver-less or explicit-this call as impure; receiver-qualified resolver and static-helper calls (Theme.surface.from(ctx), JsonUtils.parse(x)) still lint as before. Remove any // ignore: you added for these.
Maintenance
  • The extension translation pipeline now splits an over-long source string on clause boundaries (newlines, then ;:—–,, never mid-URL) when sentence-splitting alone leaves it past NLLB's per-call token gate, so long config descriptions and markdown link blocks stay on the higher-quality NLLB engine instead of silently dropping to Google. Every string NLLB still cannot translate (Google-served, left English, or unsplittable) is now named in a new reports/i18n_nllb_fallbacks.md report instead of a once-only console log, making fallbacks visible and actionable. Build tooling only (excluded from the .vsix); no behavior change for users.
  • The extension translation pipeline also splits a terminator-less source string on its semicolons and colons before the first NLLB call, even when the string sits under the per-call token gate — a colon-introduced list sent whole was degenerating into repetition and burning the full 10s deadline before bouncing to Google, and now each clause translates quickly on NLLB instead. The locale-generation run shows a live per-locale progress bar with words-per-minute and an ETA during the otherwise-silent machine-translation phase (a plain checkpoint line on non-TTY logs). Build tooling only (excluded from the .vsix); no behavior change for users.
  • Reduced ROADMAP.md to a short redirector pointing at the GitHub plans/ folder, where the live roadmap, build backlog, and deferred-rule documents already reside. The previous inline copy had drifted out of sync (broken links to plan files since moved or deleted) and duplicated content the plans/ tree owns. Doc housekeeping only.
  • The rule-pack lockfile resolver can now distinguish direct from transitive dependencies (isDirectDependency), parsing the dependency: field of pubspec.lock. This is the resolver primitive behind the ratified "direct-only suggestions" policy; the suggest UX that consumes it is not yet wired. No behavior change for users.
  • Corrected stale test paths in the plugin-system migration plan so the documented rule-pack verification command (test/config/rule_packs_*.dart) actually runs. Plan housekeeping only.
  • Re-keyed the three remaining DX-message audit scripts (_audit_dx.py, _improve_dx_messages.py, _audit.py) from the retired 5-bucket impact taxonomy to the 3-level error/warning/info severity model, so their length thresholds and per-severity report tables grade against live values instead of silently defaulting. Internal tooling only; closes SEV-04 of the LintImpact→severity collapse plan.
  • Removed 396 empty-body stub tests (test('…', () {})) plus 255 group() blocks those deletions left empty (651 statements across 47 test files). An empty test body always passes and asserts nothing, giving false coverage confidence. The stub guard now hard-gates the empty-body shape to zero via scanEmptyBodyStubTests; 27 legitimate assertion-free tests (does-not-throw and helper-asserted) are retained. Restoring behavioral coverage for the affected rules is tracked as Phase 2 in plans/BUG_stub_tests_in_suite.md. Test-suite hygiene only; no rule or runtime change.
  • The publish audit now blocks on the stub guard. run_stub_guard_check runs the guard test in the audit phase and feeds AuditResult.stub_guard_passed into has_blocking_issues, so a reintroduced always-pass stub fails the publish audit (exit code 11) instead of only failing the wave-through-able Step 7 test run. Release tooling only.
  • Reorganized CHANGELOG_ARCHIVE.md so non-user-facing entries (publish/CI tooling, audit scripts, test fixtures, internal refactors, doc/ROADMAP housekeeping) across historical releases now sit in collapsed <details><summary>Maintenance</summary> blocks instead of ### Added/### Changed/### Fixed. Content relocated verbatim; the oldest releases (2.3.5 and earlier) still have a residual loose-bullet pass pending. Archive formatting only.
  • Closed two locale-catalog gaps the MT run left identical to English: the Bengali Package Dashboard "downloads in the last 30 days" cell now reads in Bengali, and the Persian bullet-only • {dep} string is registered as a curated dictionary passthrough so the coverage gate stops flagging a punctuation-plus-placeholder string that has no translatable content. Locale catalog only; no rule or runtime change.

13.12.4 #

Clears a wide round of false positives across the string, exception-handling, async, rebuild, testing, collection, listener, lifecycle, and platform rules, so idiomatic patterns that previously forced project-local ignores now pass cleanly. The Package Dashboard gains a one-click "Save Upgrade Report" that exports just the packages with an available update as a focused worklist, and every Package Vibrancy dashboard is now fully translatable instead of always rendering in English. The localized UI also stops machine-translating brand and tool names — the product name, VS Code, and pub.dev now read identically in every language. No action required unless you added an ignore for one of the corrected patterns. log

Fixed #

  • avoid_string_concatenation_loop only fires on a genuine accumulator (s = s + x). Per-element transforms — .map((e) => e + suffix), a fresh per-iteration local, a RegExp(... + ...) argument — produce a new string each pass (O(n)), not the O(n²) accumulation the rule targets, and are no longer flagged. No action required.
  • avoid_swallowing_exceptions no longer flags a catch (_) wildcard whose body handles the error. The _ wildcard is a deliberate "discard this object" marker that cannot be referenced; a named-but-unused catch is also exempt when the body logs or rethrows. An empty catch is still flagged. No action required.
  • avoid_unawaited_future exempts close()/cancel() cleanup in synchronous void contexts. A controller close() in dispose(), a subscription cancel() in a StreamController.onCancel closure, and a close() in a hand-named teardown method are recognized — awaiting is impossible there. No action required.
  • avoid_excessive_rebuilds_animation no longer flags builders that read an animated value at a nested leaf. It now counts only the genuinely-static (hoistable) widgets — those whose subtree reads no animation .value — so a fontSize: a.value Text wrapped in required scaffold is exempt, while a large static subtree under Opacity(opacity: a.value) still fires. No action required.
  • prefer_setup_teardown no longer flags per-test pumpWidget or per-group arrange. Statements bound to a testWidgets WidgetTester cannot move to setUp(), and when the file already declares a real setUp() the duplicate threshold is raised so per-group arrange does not trip. No action required.
  • prefer_single_setstate no longer merges a setState before a loop with one inside the loop after an in-loop await. Loop bodies are scanned as their own execution scope, so calls separated by a per-iteration suspension are not reported as combinable; two consecutive setState calls in one iteration still are. No action required.
  • prefer_value_listenable_builder no longer flags async-loaded, FutureBuilder-companion, or notifier-backed single-field states. A State that holds a Future/Stream field, assigns its field in an async setState, or manually wires a Listenable is exempt (a ValueNotifier cannot replace those); genuine synchronous single-value display state still fires. No action required.
  • avoid_collection_equality_checks no longer flags ==/!= on model types whose class name merely starts with a collection keyword. It now uses a resolved-type check, so MapClusterModel, ListTileData, and SetupConfig are no longer mistaken for Map/List/Set; real List/Map/Set comparisons still fire. No action required.
  • avoid_unsafe_collection_methods recognizes seven more non-emptiness guard shapes before flagging .first/.last/.single. Combined == null || isEmpty and length <= 1 early returns, continue/break guards in loops, a guard one block above the access, Map.keys/.values after a guard on the map (or inside a while (map.length > n) loop), isListNullOrEmpty extension guards, indexed targets (m[k]!.first), and split() results held in a variable are all treated as non-empty. Genuinely unguarded access still fires. No action required.
  • avoid_listview_without_item_extent no longer flags lists where itemExtent cannot help or cannot be set correctly. A shrinkWrap: true list (any physics) is exempt because eager layout already defeats lazy extent, and a ListView.builder whose item is self-sizing (ListTile with optional subtitle, ExpansionTile, a Common*ListTile/PanelExpandable wrapper) is exempt because no constant extent is correct; plain fixed-height lists still fire. No action required.
  • always_remove_listener no longer reports a leak when add and remove reach the same listenable through different null-aware operators. The idiomatic field!.addListener(cb) in initState paired with field?.removeListener(cb) in dispose now matches. No action required.
  • avoid_context_in_initstate_dispose only fires when context performs an inherited-widget or render-tree lookup. A context forwarded to an ordinary helper (e.g. resolveColor(context)) that does no .of(context) lookup is no longer flagged; Theme.of(context), context.read(), context.size, and dependOnInheritedWidgetOfExactType still are. No action required.
  • avoid_string_substring recognizes more bounds guards and no longer flags provably in-bounds slices. It now accepts the else-branch of an indexOf ternary, a substring evaluated inside an if condition, isEmpty/isNotEmpty guards, startsWith/isEmpty/regex early-exit returns, property/index substring arguments (prefix.length, match.start, split[0]), and post-loop slices bounded by a preceding loop. No action required.
  • avoid_returning_null_for_future no longer flags return null from a function declared to return a nullable Future (Future<T>?). A nullable Future explicitly permits null, so the return is type-correct; only non-nullable Future<T> is still flagged. No action required.
  • avoid_ios_hardcoded_device_model no longer flags device names that appear as data. A device model in a list/set/map literal (e.g. an email-signature noise-filter corpus) is exempt, while a genuine == 'iPhone 14' comparison or model.contains('iPod touch') check still fires. No action required.
  • require_dialog_tests no longer flags calls that merely contain "Dialog" in their name. A localization string getter such as emergencyDirectoryDialogHeader(...) is not a dialog launch; the rule now matches known dialog launchers or *Dialog* calls that return an awaitable. No action required.
  • require_error_identification no longer flags non-color ternaries that select a log-severity enum or text label. It now requires a branch to be Color-typed, so a DebugLevels.Error selection is not mistaken for an error-color cue. No action required.
  • avoid_unbounded_cache_growth recognizes removeWhere / removeRange / clear as eviction. A cache pruned with these idiomatic operations is no longer reported as unbounded. No action required.

Added (Extension) #

  • The Package Dashboard toolbar adds a "Save Upgrade Report" button next to "Save". It writes the same per-package JSON as "Save" but filtered to only packages with an available update, to reports/YYYYMMDD/..._pubspec_upgrade.json, giving you a focused upgrade worklist. No action required.
  • Upgrade-pack nudge: offers to enable matching migration rule packs. On activation and after each pubspec.lock change (e.g. a pub upgrade), the extension checks whether your resolved package versions bring a semver-gated migration pack into range (Dio 5.x, Bloc 8.x, Riverpod 2.x/3.x, go_router 6.x) while that pack is off, and offers — once per workspace, per pack — to enable it and run analysis. It reads the lockfile and applies the same gate the analyzer enforces, so a project below the gate version is never prompted. Turn it off with saropaLints.upgradePackNudge.enabled. No action required.

Changed (Extension) #

  • The Package Vibrancy dashboards are now fully translatable. Around 265 previously-hardcoded English strings across the report, comparison, package-detail, detail, and chart webviews now resolve through the extension's localization catalog, so they translate alongside the rest of the UI instead of always rendering in English. No visible change for English users; other locales pick up the translations once the locale catalogs are regenerated.

Fixed (Extension) #

  • Brand, tool, and code-identifier names are no longer machine-translated or transliterated in the localized UI. Product and tool names ("Saropa Lints", "VS Code", "pub.dev", "OWASP", "SPDX", "Dart", "Flutter") and literal identifiers such as file names and config keys (violations.json, analysis_options, pubspec.yaml, dev_dependencies, saropa_lints) were being rendered as native words or local-script transliterations (e.g. "Saropa Fusseln", "VS Kodu", "पब.डेव", "الانتهاكات.json") across translated command titles, settings descriptions, and dashboards; every locale now shows one worldwide spelling, and the translation pipeline shields these terms so future regenerations keep them intact. No action required.
Maintenance
  • The publish script now gates the release on a CHANGELOG Overview check: the version section must open with an intro paragraph ending in a [log](.../vX.Y.Z/CHANGELOG.md) link pinned to the proposed version. A missing intro or a stale/wrong-version link prompts retry (default) / ignore / abort instead of shipping silently. Build tooling only; no behavior change for users.
  • The extension translation script gained operator controls for long NLLB runs: a graceful Ctrl-C (first press finishes the in-flight string, flushes the cache, and exits cleanly; second force-quits), a --mode selector — gaps-only / gaps + upgrade low-quality Google→NLLB / force re-translate — with an interactive menu, persistent per-string engine provenance, and --show / --set / --unset commands to inspect, override, or remove cached translations. Build tooling only (excluded from the .vsix); no behavior change for users.
  • The translation script's --mode menu adds an audit-only option (--mode audit) that writes the gaps + low-quality (upgrade-candidate) report to file without translating, pruning, or rewriting any locale, plus an [a] abort option that exits the menu with no changes. Build tooling only; no behavior change for users.
  • The publish pipeline no longer machine-translates extension locales; it now runs the coverage check in --mode audit (writes the gaps report, translates nothing) and, on a remaining gap, prompts Retry (re-audit, default) / Ignore / Abort. Closing gaps is an explicit separate step (edit dictionaries.py or run the translator yourself). Build tooling only; no behavior change for users.
  • The extension package now excludes scripts/** (build-time i18n/MT tooling) from the published .vsix. Those scripts are never loaded at runtime — runtime locales ship from src/i18n/locales via the esbuild bundle — and shipping them leaked the machine-translation cache, whose high-entropy translated strings tripped Open VSX's secret scanner and blocked publication. No behavior change.
  • The publish script now checks for VSCE_PAT before the Marketplace publish step and prompts with token-creation guidance when it is missing, mirroring the existing Open VSX handling. A missing or expired Marketplace token previously failed with an opaque vsce error and only a generic "PAT expired?" guess. No behavior change.
  • Documented the upstream cause of the analyzer/analyzer_plugin version caps in pubspec.yaml: Flutter pins meta exactly inside the SDK, analyzer 13 raised its meta floor above that pin, and the caps clear only when Flutter stable bumps its bundled meta. Added links to the Dart pinning rationale and the open Flutter unpin issue. No behavior change.

13.12.3 #

Adds a family of compound performance rules that flag GPU-expensive widgets only when a parent makes their cost recur every frame or every scrolled item, leaving intentional one-off use alone, plus a Project Map panel that ranks features by rendering risk before you profile. Also clears a broad round of false positives across the async-context, logging, disposal, date-formatting, enum-map, environment-mixing, parameter-mutation, and error-widget rules, so patterns that previously forced project-local ignores now pass cleanly. No action required unless you added an ignore for one of these patterns. log

Added #

  • Six compound performance rules — an expensive widget is flagged only when its parent makes the cost recur, never on its own. avoid_opacity_in_animated_builder, avoid_opacity_in_scrollable, avoid_backdrop_filter_in_scrollable, avoid_shader_mask_in_scrollable, avoid_image_filter_in_scrollable, and avoid_clip_path_in_animated_builder catch GPU-expensive widgets (Opacity, BackdropFilter, ShaderMask, ImageFiltered/ColorFiltered, ClipPath) placed inside an AnimatedBuilder or a scrollable, where they re-composite every frame or every scrolled item. Static, one-off use of the same widgets is intentionally not reported. Recommended tier and above.
  • The Project Map dashboard now ranks features by performance gravity. A new panel scores each feature (0–100) from the compound performance patterns its files contain, surfacing which features carry the most rendering risk before you profile. The score reflects pattern severity alone, so adding pattern-free files never changes it. Enable on the CLI with project_health --performance.

Fixed #

  • avoid_context_in_async_static no longer flags a guarded or pre-await BuildContext parameter. The rule reported the parameter unconditionally whenever the method was an async static, so a context used only before the async gap opened, or only after a mounted check, was wrongly flagged and forced widespread ignores. It now reuses the same flow analysis as the sibling after-await rule and fires only when the context is used after an await without a guard. Genuinely unguarded post-await usage still flags. No action required.
  • avoid_debug_print and avoid_print_error no longer flag the logging infrastructure's own sink. Both rules redirect callers to structured logging, but the implementation of that logging — functions named debug*, _debug*, or breadcrumb — must call debugPrint/print directly, since routing back through debug() would recurse infinitely. Both rules now exempt calls inside those logging-primitive functions. Ordinary application debugPrint/print-in-catch usage still flags. No action required.
  • avoid_duplicate_object_elements no longer flags a repeated entry in a gradient colors:/stops: list. Those parameters are ordered, position-sensitive sequences where a symmetric ramp deliberately repeats an endpoint ([base, highlight, base]), so the duplicate is required by the visual shape, not a copy-paste error. The rule now skips lists bound to a colors: or stops: argument. Duplicate identifiers, booleans, and nulls in ordinary collections still flag. No action required.
  • avoid_large_list_copy no longer flags a structurally-required .toList() in a switch arm, record field, or yield. The exemption that recognizes mandatory-List contexts climbed through transparent wrappers but stopped at a switch-expression arm, a record literal, or a yield, so a .toList() inside a List<T> getter built from a switch was wrongly flagged on every arm. It now climbs through those nodes to the enclosing return/body and treats a generator yield as requiring a List. A genuinely avoidable .toList() (lazy chain whose result is discarded) still flags. No action required.
  • avoid_manual_date_formatting no longer flags non-DateTime types or internal keys. A string interpolation reading two or more .month/.day/.year getters was treated as manual date formatting even when the value came from a custom calendar type (HebrewDate, a project DateTime wrapper) or built an internal dedup/cache key, because an unresolved type was assumed to be a DateTime. An unresolved type is now treated as non-DateTime, and the internal-key exemption now also covers values returned from or assigned inside a key/cache/hash-named function. Manual formatting of a real DateTime for display still flags. No action required.
  • avoid_missing_enum_constant_in_map no longer flags a partial map constrained by a sibling argument. A map that omits enum constants is intentional when a sibling argument on the same constructor fixes that enum to a single value — Dismissible(direction: DismissDirection.up, dismissThresholds: {DismissDirection.up: 0.25}), where the other directions can never fire. The rule now skips an enum-keyed map passed as a named argument when a sibling named argument has a value of the same enum type. A standalone partial enum-keyed map still flags. No action required.
  • avoid_undisposed_instances and require_field_dispose now recognize disposal through a multi-section cascade. A controller disposed via _c?..removeListener(f)..dispose() read as undisposed because the AST rule looked at the cascade section's (empty) syntactic target and the string-based rule's patterns only matched ..dispose() as the first cascade section. The AST rule now resolves the cascade receiver and the string rule matches dispose() in any cascade position. A field whose cascade never calls a disposal method still flags. No action required.
  • no_equal_arguments no longer flags constructors where equal arguments are required. A repeated identifier argument was treated as a copy-paste error even for a neutral gray (Color.fromRGBO(g, g, g, 1)), a point-anchor rect (RelativeRect.fromLTRB(x, y, x, y)), or a square (Size(d, d)), where the equal values are the documented idiom. The rule now exempts fromRGBO, fromARGB, fromLTRB, Size, and Offset. A repeated identifier passed to any other callee (e.g. setPosition(x, x)) still flags. No action required.
  • no_equal_nested_conditions no longer flags an inner check whose variable was reassigned. When a nested if repeats the outer condition textually but the condition's variable was reassigned in between (if (q == null) { q = q?.trim(); if (q == null) … }), the inner check tests the new value and is a mandatory guard, not redundant. The rule now tracks reassignments in the then-branch and stays quiet when a condition variable was reassigned before the inner check. A genuinely redundant inner check (no reassignment) still flags. No action required.
  • prefer_dispose_before_new_instance no longer flags a deferred post-frame disposal. When a controller field is reassigned and the previous instance is captured into a local and disposed later — typically in an addPostFrameCallback/Future.microtask closure, because disposing inline asserts while the controller is still attached — the old instance is correctly released. The rule now recognizes the capture-and-dispose idiom (including disposal inside a callback closure). A reassignment with no disposal of the prior instance still flags. No action required.
  • prefer_layout_builder_for_constraints no longer flags a window-width breakpoint query. MediaQuery.sizeOf(context).width passed as a positional argument to a device-class helper (ResponsiveLayout.isWide(MediaQuery.sizeOf(context).width)) reads the window width to pick a layout strategy, not to size a widget — LayoutBuilder would give the local box width, the wrong signal. The rule now exempts a width/height used as a positional call argument. A width/height assigned to a width:/height: named argument (actual widget sizing) still flags. No action required.
  • prefer_reusing_assigned_local no longer flags a post-increment index or a repeated constructor allocation. An index read with a side effect (pts[i++]) advances the index each evaluation, and ValueNotifier<bool>(false) declared twice allocates two distinct objects, so neither is a redundant recompute — yet both were flagged as reusable. The rule now treats ++/-- in the initializer and PascalCase constructor-style calls as non-reusable. A genuine repeated pure read still flags. No action required.
  • require_error_widget no longer flags a builder: method tear-off that handles the error. When a FutureBuilder/StreamBuilder builder: was a method reference (builder: _buildContent) rather than an inline closure, the rule fell back to a substring check of the identifier name and so fired on every tear-off, even when the referenced method fully handled snapshot.hasError. It now resolves the referenced method by name and inspects its body, suppressing rather than reporting when the method cannot be located (cross-library tear-off). A tear-off whose body ignores the error state still flags. No action required.
  • avoid_mixed_environments no longer flags environment keywords that appear only as substrings of unrelated identifiers. The rule matched prod/release/test/dev/live/local as raw substrings, so a config class with release_notes and latest (or developer, delivery, locale) was wrongly reported as mixing production and development configuration. It now tokenizes identifiers into whole words — splitting on non-letters and camelCase — and matches keywords by exact word, so release_notes/latest are ignored while genuine mixes (apiUrlProd alongside debugFlag) still flag. No action required.
  • avoid_parameter_mutation no longer flags index assignment into a List, typed-data, or Map parameter. Filling a caller-allocated buffer by index (p[i] = value on a List/Uint8List/Map passed in to be populated) is the out-parameter/output pattern — the same intent already exempted for .add/.addAll — yet it was flagged unconditionally, producing hundreds of false positives on generated fill-buffer tables. Index assignment into a collection parameter is now exempt; field and cascade-field assignment on a DTO parameter (the real caller-corruption case) still flags. No action required.
Maintenance
  • Reworked the unreleased prefer_dispose_before_new_instance deferred-dispose helper to walk the AST instead of substring-matching block.toSource(), removing a source.contains() anti-pattern that the CI guard rejects and matching the disposed receiver exactly across plain, null-aware, and cascade forms. No behavior change.
  • Added regression fixtures for avoid_equal_expressions covering compound arithmetic with identical operands (dx * dx + dy * dy, (a * a + b * b) / 2). The rule already excludes arithmetic operators (fixed in 13.12.2); these cases guard against a future regression. No behavior change.
  • The extension localization tooling can now use Meta NLLB-200-3.3B (local, offline) as the primary machine-translation engine, with Google Translate as the per-string fallback — substantially higher quality on low-resource languages where the free web engine produces near-gibberish. It activates automatically when the model is on disk (reusing a download shared with other Saropa tooling) and falls back to Google otherwise; SAROPA_SKIP_NLLB=1 forces Google-only and nllb_engine.py --setup downloads the model. No shipped locale strings change until the locales are regenerated and committed. No behavior change for the lint package.

13.12.2 #

Fixes a nullify_after_dispose false positive that flagged dispose()/cancel()/close() calls on local variables, a prefer_single_setstate false positive that flagged setState calls sitting in mutually-exclusive branches, an avoid_equal_expressions false positive that flagged arithmetic with identical operands such as 1024 * 1024, and an avoid_variable_shadowing false positive that flagged a name legitimately reused in disjoint sibling scopes (two collection-for loops in separate literals, or the same local in two separate switch cases). No action required unless you added a project-local ignore for these patterns. log

Fixed #

  • avoid_variable_shadowing no longer flags a name reused in a disjoint sibling or sequential scope. Shadowing is a nesting relationship, but the checker tracked declared names in one flat set for the whole body and added the loop variable of a collection-for (inside a list/set/map literal) and locals in brace-less switch cases without ever removing them when the scope closed, so reusing the same name in a second sibling literal or a later case was wrongly reported. It now snapshots and restores the name set around each collection-for element and each switch case, matching the existing handling for statement-level loops and if/else blocks. Genuinely nested shadows (an inner block, loop, or closure hiding a still-live outer name) still flag. No action required.
  • nullify_after_dispose no longer flags a disposable local variable. The rule targets nullable instance fields, but it treated any disposed SimpleIdentifier as a non-final nullable field, so disposing a method-local (such as a final ui.Codec) was falsely reported even though a local cannot be nulled and needs none. It now confirms the target is a declared field of the enclosing class before reporting. Genuine nullable fields disposed without nullification still flag. No action required.
  • prefer_single_setstate no longer flags setState calls in mutually-exclusive branches or across an await. The rule counted every setState in a method body together, so calls in separate if/else arms, distinct switch cases, a try body versus its catch, or on opposite sides of an await (the setState(busy=true); await …; setState(busy=false) loading-state idiom) were reported as mergeable even though they can never run together in one synchronous pass. It now treats each branch as its own scope and an await as a segment boundary, flagging only when two or more setState calls share one synchronous straight-line path. Genuine sequential setState calls still flag. No action required.
  • avoid_equal_expressions no longer flags arithmetic with identical operands. The matcher previously fired on any binary expression whose left and right source text matched, sweeping in legitimate constant math like 1024 * 1024, 60 * 60, x * x, and 1 << 1. It now restricts the identical-operand check to comparison (==, <, >, <=, >=) and logical (&&, ||) operators, where two identical sides always produce a constant or redundant result. Genuine copy-paste bugs (a == a, w > w, flag && flag) still flag. No action required.

13.12.1 #

Fixes two prefer_value_listenable_builder false positives: one on State classes that back a FutureBuilder/StreamBuilder cache with a plain cache-key companion field plus one setState that re-runs the fetch, and one on screens whose extra rebuild state lives in a final controller republished by a bare setState(() {}). Also fixes two prefer_reusing_assigned_local false positives: one where a nested builder closure reuses a parameter name (such as snapshot) that an outer scope already declared, and one where a live value (such as a GlobalKey's currentContext) is intentionally re-read after an await. Renames the impact_report CLI tool to severity_report (the old name still works). No action required unless you added a project-local ignore for one of these patterns. log

Fixed #

  • prefer_reusing_assigned_local no longer flags a read re-evaluated after an await. A suspension point lets external state change between two reads (for example a GlobalKey's currentContext flipping from null to mounted while the navigator boots during an awaited delay), so the cached value is no longer guaranteed equal and reusing it would reintroduce a cross-async-gap bug. The rule now treats an intervening await as a barrier and stays quiet; redundant re-reads with no await between them still flag. No action required.
  • prefer_reusing_assigned_local no longer flags an identical expression read against a shadowed inner variable. When a nested closure parameter (such as a FutureBuilder builder whose snapshot shadows the outer StreamBuilder snapshot) reads the same member text as an outer local, the rule now confirms the identifiers resolve to the same binding before reporting, so reuse is never suggested across a shadow where it would read the wrong value or fail to compile. Genuine same-binding recomputes still flag. No action required.
  • prefer_value_listenable_builder no longer fires on the cached FutureBuilder idiom when the cache carries a non-Future key field. A non-Future field now counts as single-value state only when it is reassigned inside a setState callback, so a cache key mutated only in helper methods (or re-initialized via a setState tear-off) is correctly ignored. Genuine single-value setState state stays flagged. No action required.
  • prefer_value_listenable_builder no longer fires when a State also rebuilds via a bare setState(() {}) whose callback assigns no field. A bare rebuild signals state held outside the counted fields — a final TextEditingController/Listenable or a parent value — that a single ValueListenableBuilder cannot model, so the rule now stays quiet (this also covers the common setState(() => cb?.call()) safe-setState wrapper). Genuine single-value state still triggers. No action required.

Changed #

  • Renamed the impact_report CLI tool to severity_report to match the three-level severity model (errors / warnings / info) that replaced the old five-bucket impact grades. Run dart run saropa_lints:severity_report; the old dart run saropa_lints:impact_report keeps working as an alias, so no action required.
Maintenance
  • Fixed the publish audit's "Rules by Severity" table, which had been counting zero for every rule since the impact taxonomy collapsed because it still keyed on the retired critical/high/medium/low value set instead of error/warning/info.

13.12.0 #

Adds support for analyzer 12, now that Flutter stable ships the meta version analyzer 12 requires. Flutter and Dart projects on analyzer 12 can use saropa_lints without being pinned back to analyzer 11. analyzer 13 stays excluded because it requires a newer meta than Flutter stable currently pins. No action required. log

Changed #

  • Added analyzer 12 support: widened the analyzer/analyzer_plugin constraints and updated the plugin internally for analyzer 12's API changes, so it now compiles and runs on analyzer 11 and 12. analyzer 13 remains excluded because it demands a newer meta than Flutter stable pins, which would break flutter pub add. No action required.

Fixed #

  • The "issues found" popup shown after analysis no longer claims violations when the Findings dashboard is empty, and no longer offers dead "Copy Report" / "Open Report" buttons. It now waits for the analyzer to finish writing its results before appearing, counts what the dashboard will actually display so the two always agree, and shows each button only when the report it opens exists. When a run produces no findings it shows an honest "see Output" message instead. No action required.

Historical Changelog Archive #

Looking for older changes? See CHANGELOG_ARCHIVE.md for versions 0.1.0 through 13.1.4.

7
likes
0
points
6.31k
downloads

Publisher

verified publishersaropa.com

Weekly Downloads

2134 custom lint rules with 254 quick fixes for Flutter and Dart. Static analysis for security, accessibility, and performance.

Homepage
Repository (GitHub)
View/report issues

Topics

#linter #static-analysis #code-quality #flutter #dart

License

unknown (license)

Dependencies

analysis_server_plugin, analyzer, analyzer_plugin, collection, meta, path, pub_semver, yaml

More

Packages that depend on saropa_lints