saropa_lints 14.0.2
saropa_lints: ^14.0.2 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.
Package — pub.dev/packages/saropa_lints
Releases — github.com/saropa/saropa_lints/releases
VS Code Marketplace — marketplace.visualstudio.com/items?itemName=saropa.saropa-lints
Open VSX Registry — open-vsx.org/extension/saropa/saropa-lints
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 theapp_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, andrequire_speech_stop_on_disposenow 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, andconfigSuggestions.badgeTooltipwere 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
isGeneratedDartPathhelper 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 ageneratedpath 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.tsgained the spacing / radius / type / elevation / motion / z-index tokens (plus an exportedgetDashboardTokens()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-secondaryBackgroundundefined 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-glowvalue now matchchromeTokens()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.openDashboardscommand 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
mainno longer triggers a redundantcirun. 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/testWidgetsstubs 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 likeapp_links,image_picker,webview_flutter,url_launcher,geolocator,google_sign_in,firebase, plus the six new packs above). If youranalysis_options.yamllistspackage_specificunderrule_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_constructorno longer flagslist.add(...)orcontroller.add(...)in a Bloc constructor. It flagged every method namedadd, so populating a local list or stream controller during construction was wrongly reported as dispatching a Bloc event; it now flags only an unqualifiedadd(event)on the Bloc itself. No action required. -
avoid_ref_in_disposeandavoid_ref_inside_state_disposeno longer flag an unrelated field or local namedrefused indispose(). They reported anyrefby name, so a non-Riverpod object namedrefwas wrongly flagged; they now report only whenrefresolves to Riverpod'sWidgetRef/Ref. No action required. -
avoid_nullable_async_value_patternno longer flags.valueon a variable merely named with "async". It matched the variable name, so anyasyncThing.valueon an unrelated type was wrongly reported; it now flags only.valueon a realAsyncValue. No action required. -
require_immutable_bloc_stateno longer flags a...State-named class that is not a Bloc state. Any concrete class whose name ended inState(such as aRequestStatevalue object) was flagged purely by name; it now fires only when the class is used as aBloc/Cubitstate type argument. No action required. -
prefer_cubit_for_simplenow counts event handlers from the code, not from comments or strings. The handler count came from a text scan that matchedon<...>tokens inside comments and string literals and missed nested generics likeon<Wrapper<int>>; it now counts realon<Event>(...)registrations, fixing both over- and under-counting. No action required. -
avoid_logging_sensitive_datano longer flags logging an OAuth flow identifier such asoauthToken. The intended OAuth carve-out never worked and the rule treated thetokeninsideoauthTokenas a leaked secret; it now suppresses only when the sensitive word is part of a known safe identifier. No action required. -
prefer_streaming_responseno longer flags.bodyor in-memory uses of.bodyBytes. It also fired on the String.bodyand on any.bodyBytesthat merely shared a scope with an unrelatedfilevariable; it now flags only.bodyByteswritten to disk in the same function. No action required. -
avoid_over_fetchingno longer flags ordinary repository methods that fetch and return a single field. A short method likefinal 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_refreshno 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_clientno longer flags constructing a JWT model object outside an authorization check. Building a value likeJsonWebToken.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_fallbacknow catches the canonicallocalAuth.authenticate(biometricOnly: true)call. The receiver-name match missed single-token receivers likelocalAuth, so biometric-only logins went unflagged; it now matches the local_auth biometric API. No action required. -
avoid_webview_javascript_enabledno longer fires when JavaScript is disabled but an unrelated flag (e.g.isInspectable: true) is set. The rule now reads the actual boolean value ofjavaScriptEnabledinstead of scanning the argument text fortrue, soInAppWebViewSettings(javaScriptEnabled: false, isInspectable: true)stays silent. No action required. -
prefer_cached_getterno 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-declaredgetaccessor; 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_listnow flags only StatefulWidget list items,avoid_duplicate_widget_keystreatsKey('x')andValueKey('x')as distinct,avoid_gesture_conflictrequires direct nesting,prefer_tap_region_for_dismissno longer trips onpopulate/closest,prefer_asset_image_for_localmatches onlyassets/paths,avoid_nullable_widget_methodsno longer flags non-widget types named "…Widget",avoid_double_tap_submitno longer treats a "Reorder" button as submit,avoid_late_without_guaranteedetects a realinitStateassignment,avoid_static_route_configmatches exact router types, andprefer_split_widget_constcounts only const-constructible children. Fewer spurious warnings; no action required. -
avoid_string_concatenation_loopno longer flags numeric+=accumulators. A loop body liketotal += countorresultMap[k] += nwas reported as O(n²) string concatenation based only on the variable name containingresult/output/buffer/message. The+=branch now requires the accumulator to actually be aString(by resolved type), so numeric accumulation stays silent while genuineString += ...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, andavoid_set_state_in_disposewere silently dead — they tested a method's direct parent forClassDeclaration, 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_initstateandavoid_expensive_did_change_dependenciesno longer fire on a same-namedinitState/didChangeDependenciesmethod declared on a plain (non-State) class.avoid_unsafe_setstateno longer treatssetState()in theelsebranch ofif (mounted)as guarded.prefer_widget_state_mixinno longer counts unrelated members like_unfocusTimer/_focusableItemsas interaction-state fields (whole-word, bool-typed match).avoid_scaffold_messenger_after_awaitwalks the body in source order so aScaffoldMessenger.of(context)call lexically before the await is not reported as "after".require_field_disposedetects 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_disposeskips controllers read fromwidget.*(parent-owned).avoid_recursive_widget_callsmatches self-instantiation by resolved element, so a same-named imported widget is no longer flagged.require_init_state_idempotentmatchesremoveListener/removeObserveras 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_misuseflags only a state-toggled ternary, so a static const such asopacity: _kDisabledOpacitystays silent;prefer_fractional_sizingmatches a realMediaQuery…size.width|heightread by structure, so a same-named local likefakeMediaQuery.size.width * 0.5no longer trips;prefer_page_storage_keyrecognizes aPageStorageKeyby type and is no longer fooled by lookalike names;avoid_stack_without_positionedskips children of aStackthat sets its ownalignment/fit(the intentional badge-over-avatar pattern);prefer_spacing_over_sizedboxtreatsSizedBox(height: 8)and8.0as the same spacer;avoid_builder_index_out_of_boundscompares full receiver chains so a guard onother.items.lengthno longer excusesitems[index], catching a real out-of-bounds risk; andavoid_hardcoded_layout_valuesdrops a dead duplicate branch with no behavior change. No action required. -
require_stream_error_handlingno longer flags a non-Stream object whose name ends in "controller". A call likeanimationController.listen(...)was reported for a missingonErrorbecause the name-based fallback treated any "…controller" receiver as a stream. It now requires the resolvedStreamtype, an explicit.streamaccess, or a name ending in "stream"; a bareAnimationController/ScrollControllerstays silent. No action required. -
prefer_utc_for_storageno longer flags atoIso8601String()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 anysave(/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_invocationsno longer flags an unrelateddart:iocall such assystemEncoding.decode(...). A catch-all final clause matched anydart:ioelement whose method name appeared in the throwing-call set, so non-I/O members nameddecode/parsewere wrongly reported; detection is now restricted to the documented throwing calls qualified by both library and method (dart:ioread*/write* sync I/O,dart:convertdecode/jsonDecode,dart:coreparse). No action required. -
avoid_dialog_context_after_asyncno longer misjudges amountedcheck 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 afterNavigator.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 latermountedcheck is correctly flagged. No action required. -
prefer_no_commented_out_codeno longer flags a wrapped prose sentence that cites a function call. A multi-line comment such as "…Without this,formatNumberLocale(x, decimalPlaces: 25)crashed (formatDoublein…" 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 containingprefix,suffix,fixture, ortoStringAsFixedbeing misread asFIXtask markers (marker words now match only as whole words), which also affectsprefer_capitalized_comment_start. No action required. -
prefer_setup_teardownno 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 intosetUp()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_guardsno longer fires on routes whose path merely contains a protected word. This ERROR-level rule treated/reorderas the protected segmentorderand/accountingasaccount(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.accountoradminis flagged. No action required. -
avoid_go_router_push_replacement_confusionno longer flags non-detail routes by substring.context.go('/viewport/$x')matched the detail segment/viewand was reported; matching is now per path segment, so/viewportno longer trips theviewrule. No action required. -
avoid_push_replacement_misuseno longer fires on builder bodies and route objects. It lowercased the whole argument source, soMaterialPageRoute(builder: () => ListView())matchedviewand anOrder…builder matchedorder. It now inspects only the route name (thepushReplacementNamedstring andRouteSettings(name:)) by segment. No action required. -
require_deep_link_testingno longer treatsuuid:/valid:as an id. The rule checked whether the route argument source containedid:, so an object built with auuid:orvalid:argument was wrongly considered to carry a routable id and skipped. It now inspects argument labels and map keys for an exactid, so objects lacking one are correctly flagged. No action required. -
avoid_pop_without_resultno longer confuses similarly-named variables. Aresultfromawait Navigator.push(...)was considered "used without a null check" when an unrelatedresultValueappeared 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_constraintsno longer flags two legitimateMediaQuerysizing patterns. It fired on aMediaQuery.sizeOfread that is the fallback branch of a constraint-finiteness guard (constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.sizeOf(context).width) — code that already usesLayoutBuilderand only consultsMediaQuerywhen the parent passes unbounded constraints, where there is noLayoutBuildervalue to use. It also flagged a screen-fraction height scaled by a named factor (MediaQuery.sizeOf(context).height * fraction) inside an unbounded scroller, whereLayoutBuilderwould 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 genuinewidth: MediaQuery.sizeOf(context).widthsizing keeps flagging. -
Line-level
// ignore:no longer over-suppresses rules whose name is a prefix of another. An// ignore: my_rule_extendedcomment was matched by a bare substring check, so it also silenced the distinct, shortermy_ruleon 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.dartwas matched againstmy_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_tostringno longer fires onFutureOrvalues, 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 withFuture— which wrongly flaggedFutureOr<T>(a false positive on.toString()/interpolation) and missed types that implementFuture. They now use a proper type check (Future or a Future subtype, excludingFutureOr), removing the false positives and catching previously-missed Future subtypes. -
avoid_expensive_buildno longer flags cheap built-in conversions inbuild().int.parse,double.tryParse,DateTime.parse, andUri.parsewere matched purely by the method nameparse/tryParse, so these common, inexpensive calls were reported as expensive build-time work. The rule now skipsparse/tryParsewhose resolved result is a core primitive while still flagging heavy parsing such asjsonDecode. -
incorrect_firebase_event_nameno longer fires on non-Firebase analytics. This ERROR-level rule flagged anylogEvent(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_indexandrequire_database_migrationno 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_storageno longer fires without a Drift import. It flagged anyWebDatabase(...)constructor or any method namedunsafeIndexedDb, even in code that does not use Drift; it now requires adrift/drift_flutterimport like every other Drift rule. -
avoid_throw_in_catch_blocknow 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, userethrow/Error.throwWithStackTraceor disable the rule. -
Android permission checks no longer match a longer permission that shares a prefix. A check for
READ_CONTACTSalso matched a manifest declaring onlyREAD_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/locationstring.hasIosBackgroundAudioConfigured/...LocationConfiguredchecked for the mode string anywhere in Info.plist, so a plist enabling only backgroundlocationthat mentionedaudioelsewhere reported audio as enabled. The mode is now matched inside theUIBackgroundModesarray.
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_libs→isar_community/isar_community_flutter_libs,hive_flutter→hive_ce_flutter), the last Flutter Community Plus gap (wifi_info_flutter→network_info_plus), discontinued packages pub.dev itself names a replacement for (the AngularDart packages →ng*,uni_links→app_links,artemis→ferry,super_enum→freezed, and more), and abandoned widgets/plugins (flutter_web_auth→flutter_web_auth_2,pdf_render→pdfrx,nfc_in_flutter→nfc_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: trueoverride; 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, andsaropaLints.openFindingcommands 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_advisorbut 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_stylestuck because another dependency pinsanalyzerlow, or a package pinned by the Flutter SDK's exactcharacters/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. -
constrainedpackages now name the constraint holding them back. A package the resolver could lift but your own pubspec caps now shows "your constraint^xcaps this —yresolvable" 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.siblingRepoPathsto other repo folders and a package pinned at a lower major than a sibling (e.g.saropa_lints ^9.7.0here vs^13.12.7elsewhere) 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.watchIntervalHoursto0(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_lintsdogfoods itself,dart analyzereports the plugin dependency as a constraint (^14.0.0) whilepubspec.yamlholds 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 ownanalysis_options.yamlhas no pluginversion: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 anytest/testWidgetscarrying askip:argument; a genuine() {}stub with noskip: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-nowtest 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/andbin/for real import/export directives and fails if an imported package is absent from pubspecdependencies. This catches the class of defect that sank v13.12.6 and v13.12.7 (ametaimport with nometadependency):lib/**is inanalyzer.excludefor plugin dogfooding, so nodart analyzerun inspects these imports, anddart pub publishonly rejected them on the post-tag CI job — after the tag was burned. The gate is deterministic and Dart-version-independent, and ignorespackage: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 theanalyzejob, so a missing dependency fails at merge time rather than at release. The pre-existingdart pub publish --dry-runCI 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 uxrenders 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,
buildDetailCardand 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 watchstep 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. Adependencies: # commentline 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_versiononly 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_sectionandrename_unreleased_to_versionused 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.yamlplus 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.dartruns 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 countedexpect_lintstrings, 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/showErrorMessagecalls (and their action-button labels) across the extension were routed throughl10n()with{token}interpolation instead of hardcoded English or string concatenation — 208 keys under a newnotify.*namespace inen.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, Indonesianinfo, 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.pyrelaunches under the standardpython.exebeside 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 underpython3.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.pynow 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
esbuilddevDependency to 0.28.1 to clear GHSA-gv7w-rqvm-qjhr. The advisory's RCE only affects esbuild's Deno install path via a maliciousNPM_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-quotecritical advisory (GHSA-w7jw-789q-3m8p) in the extension's dev dependency tree. A transitive dev-only dependency was bumped vianpm audit fix; it is build/test tooling and never ships to users. Two remaining low-severitydiff-via-mochaadvisories 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_intentrules.rsi_missing_initial_media(Essential) flags a file that subscribes togetMediaStream()(warm-share) but never callsgetInitialMedia()— silently dropping every cold-start share;rsi_missing_reset_after_initial_mediaflagsgetInitialMedia()in a class that never callsreset()(stale intent re-delivered on resume);rsi_unfiltered_shared_media_type(INFO) flags a handler that readsSharedMediaFilefields without ever checking.type. Run only in files that importreceive_sharing_intent. No action required. -
Six
sign_in_with_applerules. FlagsgetAppleIDCredential()not wrapped in a try/catch (every failure, including user-cancel, throws), a catch that never handlesAuthorizationErrorCode.canceled, a file that never checksisAvailable(),identityToken/givenName/familyName/email(all nullable, name+email only returned on first sign-in) assigned to non-nullableString, and a discardedgetCredentialState()result. The token rule maps to OWASP M3. Run only in files that importsign_in_with_apple. No action required. -
Five
lottierules. Flags aLottie.*factory with acontroller:but noonLoaded:(animation frozen at frame 0),Lottie.networkwith noerrorBuilder:(blank widget on load failure) or nobackgroundLoading: true(JSON parsed on the UI thread),FrameRate.maxwith norenderCache:(quadrupled repaints on 120 Hz), andRenderCache.raster(a documented memory-pressure risk). The receiver is resolved to the Lottie type, soImage.networkand friends never trip. Run only in files that importlottie. No action required. -
Six
flutter_animaterules. Flags an unconditionalcontroller.repeat()inonPlay(an infinite off-screen animation that burns battery),Animate.restartOnHotReload = trueshipped without akDebugModeguard (ERROR), an.animate()/Animate(...)list element with nokey:(animation restarts on rebuild),AnimateList(children: [])/[].animate()(dead code), a literaltarget:(state-driven param hard-coded), andautoPlay: falsewith no controller/adapter/target driver (animation never starts). Run only in files that importflutter_animate. No action required. -
Seven
awesome_notificationsrules. Flags an instance (non-static) or wrong-parameter-typesetListenershandler (ERROR — both fail at runtime), a static handler missing@pragma('vm:entry-point')(tree-shaken in release), aNotificationContent(channelKey:)literal not declared in the same file'sinitialize()channel list (notification silently discarded),createNotification()with noisNotificationAllowed()guard, a negative notification id (with a quick fix — negatives are silently randomized, breakingcancel(id)), and notification calls ordered beforesetListeners(). Run only in files that importawesome_notifications. No action required. -
share_plusmigration + correctness rules.prefer_shareplus_instance(gatedshare_plus >= 11.0.0, with a quick fix) rewrites the deprecated staticShare.share/shareUri/shareXFilestoSharePlus.instance.share(ShareParams(...)), preservingsharePositionOrigin. Four always-on rules flag aShareParamswith nosharePositionOrigin(iPad crash), a discardedShareResult, all-empty content fields (ERROR), and auri+textconflict (ERROR). No action required. -
sensors_plusmigration + best-practice rules.prefer_sensors_event_stream(gatedsensors_plus >= 4.0.0, with a quick fix) rewrites the deprecatedaccelerometerEvents-style getters to theaccelerometerEventStream()functions. Three always-on rules flag a missingsamplingPeriod,SensorInterval.fastestInterval(max battery drain), and a.listen()with noonError:. No action required. -
flutter_svgmigration + correctness rules.prefer_svg_color_filter(gatedflutter_svg >= 2.0.0, with a quick fix) rewrites the deprecatedcolor:/colorBlendMode:onSvgPicture.*tocolorFilter: ColorFilter.mode(...); the receiver is resolved toSvgPicturesoIcon/Containercolor:never trips. Four always-on rules flagSvgPicture.network/.stringwith noerrorBuilder:, network with noplaceholderBuilder:, and anySvgPicturewith nosemanticsLabel/excludeFromSemantics. No action required. -
Four
file_pickerdeprecation rules.file_picker_deprecated_allow_compression(gatedfile_picker >= 10.0.0, with a quick fix mappingallowCompression: true→compressionQuality: 75);file_picker_deprecated_with_data/with_read_stream/allow_multiple(gatedfile_picker >= 12.0.0). All relocated into the gated packs so a project on the old major never sees them. No action required. -
connectivity_pluspre-upgrade migration rule.avoid_pre_v6_single_connectivity_result(gatedconnectivity_plus < 6.0.0, with a partial quick fix== ConnectivityResult.x→.contains(...)) flags single-value handling that breaks on the v6List<ConnectivityResult>change;connectivity_satellite_missingflags an if-else chain overConnectivityResultmissing the v7.1satellitecase. No action required on v6+. -
google_sign_inmigration + v7-usage rules.avoid_pre_v7_google_sign_in(gatedgoogle_sign_in < 7.0.0) flags the removedGoogleSignIn()constructor /signIn(). Five usage rules (gatedgoogle_sign_in >= 7.0.0) flagauthenticate()with no try/catch, nosupportsAuthenticate()guard,.accessTokenread off an account (ERROR — null in v7), a catch that ignorescanceled, andauthenticate()beforeinitialize(). No action required. -
Four
local_auth3.0 pre-upgrade migration rules. Gatedlocal_auth < 3.0.0:local_auth_deprecated_options_classflags the removedAuthenticationOptions,local_auth_use_error_dialogs_removedflags its removeduseErrorDialogs(ERROR),local_auth_sticky_auth_renamedrenamesstickyAuth:→persistAcrossBackgrounding:(quick fix), andlocal_auth_platform_exception_catchrewrites aPlatformExceptioncatch toLocalAuthException(quick fix). No action required on v3+. -
Three
app_links6.0 pre-upgrade migration rules. Gatedapp_links < 6.0.0:app_links_use_get_initial_linkandapp_links_use_get_latest_linkrename the removedgetInitialAppLink()/getLatestAppLink()togetInitialLink()/getLatestLink(), andapp_links_use_uri_link_streamrenames the removedallUriLinkStream/allStringLinkStreamgetters touriLinkStream/stringLinkStream— each with a quick fix, relocated into the gatedapp_links_6pack so a project on v6+ never sees them. No action required on v6+. -
Eight
geocodingrules.geocoding_unchecked_first(ERROR) flags.first/.laston a geocoding result with no emptiness guard (the common StateError crash);geocoding_missing_exception_handlerflags a lookup not in a try/catch;geocoding_prefer_no_result_found_catch(INFO) flags catchingPlatformExceptionbut notNoResultFoundException;geocoding_locale_set_before_call(INFO) andgeocoding_concurrent_locale_racecover the v3setLocaleIdentifierAPI and its concurrency race;geocoding_missing_is_present_check(INFO) flags a lookup with noisPresent()gate;geocoding_call_in_text_field_listenerflags per-keystroke geocoding with no debounce;geocoding_deprecated_locale_param(ERROR) flags the removedlocaleIdentifier:argument. Run only in files that importgeocoding; the crash + migration rules are Recommended, the rest Comprehensive. No action required. -
Five
quick_actionsrules for the app-shortcut initialization contract and ShortcutItem fields.quick_actions_set_before_initializeandquick_actions_missing_initializeflag shortcuts that register before, or without, theinitialize(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, andquick_actions_flutter_asset_iconflag invalidShortcutItemarguments that render a dead, blank, or icon-less shortcut. All five run only in files that importquick_actionsand live in the Professional tier. No action required. -
Five
in_app_reviewrules for the review-prompt contract.in_app_review_missing_availability_checkflagsrequestReview()with noisAvailable()guard;in_app_review_button_callback_requestandin_app_review_request_in_init_stateflag prompting from a button tap or ininitState(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 noopenStoreListing()escape hatch;in_app_review_ios_store_listing_missing_app_idflagsopenStoreListing()without anappStoreIdon projects that target iOS or macOS. All five run only in files that importin_app_reviewand live in the Comprehensive tier. No action required. -
Five
local_authbiometric-auth rules.local_auth_unchecked_resultflags a discardedauthenticate()result (cancel reads as success);local_auth_missing_capability_check(INFO) flags a file with nocanCheckBiometrics/isDeviceSupported()guard;local_auth_unhandled_exceptionflags anauthenticate()not wrapped in a try/catch coveringLocalAuthException(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 missingbiometricOnly: true. The first three map to OWASP M3 (Insecure Authentication). The 3.0 migration rules (AuthenticationOptionsremoval,stickyAuthrename,PlatformExceptioncatch) are tracked separately. No action required. -
Six
file_pickercorrectness rules.file_picker_unchecked_null_resultflags using aFilePickerResult?member with no null check (cancel returns null);file_picker_path_on_web(experimental) flags force-unwrappingPlatformFile.path(null on web);file_picker_custom_type_missing_extensions(ERROR) andfile_picker_extensions_without_custom_typeflag theFileType.custom/allowedExtensionscontract;file_picker_extension_with_dotflags (and fixes) a leading-dot extension;file_picker_with_data_large_filesflagswithData: truewithallowMultiple: true(OOM risk). All run only in files that importfile_pickerand live in the Comprehensive tier. The version-gatedwithData/withReadStream/allowMultiple/allowCompressiondeprecation rules are tracked separately. No action required. -
Seven
device_calendarrules.device_calendar_missing_permission_check(INFO) flags data operations in a file with nohasPermissions/requestPermissions;device_calendar_unchecked_resultflags a discarded awaitedResult;device_calendar_result_data_before_success_checkflags readingResult.datawith noisSuccessguard;device_calendar_retrieve_events_empty_paramsanddevice_calendar_retrieve_events_missing_end_dateflag invalidRetrieveEventsParams;device_calendar_event_missing_calendar_id(ERROR) flags anEventwith nocalendarIdpassed tocreateOrUpdateEvent;device_calendar_event_utc_timezoneflagsTZDateTime.utcon an event (the Android wrong-local-time bug). All seven run only in files that importdevice_calendarand live in the Comprehensive tier. No action required. -
Six
google_maps_flutterrules. Flags per-frameSet<Marker/Polyline/…>andBitmapDescriptorrebuilds plusanimateCamera/moveCameracalls insidebuild()(map jank and queued platform calls), the deprecatedcloudMapId:argument (with a rename fix tomapId:) andsetMapStyleAPI, and unguarded info-window calls that throwUnknownMapObjectIDErrorsince 2.0. All run only in files that importgoogle_maps_flutter; the crash and safe-migration rules are Recommended, the rest Professional/Comprehensive. No action required. -
Six
audioplayersrules for audio-specific traps the generic media rules miss. FlagsonPositionChanged/onDurationChanged/onPlayerCompletelisteners andseek()on aPlayerMode.lowLatencyplayer (events that never fire), anonPlayerCompletelistener underReleaseMode.loop, an undisposedAudioPoolfield, aUrlSourcebuilt from a bundledassets/path (with a fix toAssetSource), and asetVolume/play(volume:)literal above 1.0. All run only in files that importaudioplayers. No action required. -
Six
flutter_maprules.flutter_map_missing_user_agent(Recommended) flags aTileLayerwith nouserAgentPackageName(OpenStreetMap blocks unidentified traffic, so tiles silently fail in production); the rest flag the v8tileSize/labelPlacementand v6MapOptionscenter/zoomdeprecations (two with rename fixes), aNetworkTileProviderfallbackUrlthat disables the in-memory cache, and aTileLayerwith noerrorTileCallback. All run only in files that importflutter_map. No action required. -
Five
youtube_player_flutterrules for the v10 iframe rewrite. Flags aYoutubePlayerControllerfield neverclose()d (its cleanup isclose(), notdispose(), so generic disposal rules miss it),convertUrlToId(...)used without a null check, the deprecatedYoutubePlayerScaffoldwrapper, an unmutedautoPlay(blocked by browser policy), andautoFullScreenwith no orientation/pop guard. All run only in files that importyoutube_player_flutter. No action required. -
Five new
image_pickerrules covering gaps in the existing coverage.image_picker_missing_retrieve_lost_dataflags a file that picks media but never callsretrieveLostData()(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 animageQualityliteral outside the asserted range (a confirmed iOS 16+ crash);image_picker_camera_source_without_support_checkflagsImageSource.camerawith nosupportsImageSource/platform guard (it throws on web/desktop);image_picker_lost_data_empty_check_missingflags reading aLostDataResponsewith noisEmptyguard;image_picker_multi_result_unchecked_empty(ERROR) flags[index]access on apickMultiImage/pickMultipleMediaresult with no emptiness guard (cancel returns an empty list, so it throws RangeError). All five run only in files that importimage_pickerand complement the package's existing result-handling, size, and source rules. No action required. -
Six
home_widgetrules for the home-screen-widget contract.home_widget_callback_missing_pragmaandhome_widget_callback_not_top_levelflag 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_updateflagssaveWidgetDatawith noupdateWidgetin the same member (the widget shows stale data);home_widget_update_no_nameflagsupdateWidget()with no target name (a silent no-op);home_widget_ios_missing_app_groupflagssaveWidgetData/getWidgetDatain a class that never sets the iOS App Group;home_widget_widget_clicked_without_initial_launch(INFO) flags listening towidgetClickedwith no cold-startinitiallyLaunchedFromHomeWidget()check. All six run only in files that importhome_widgetand live in the Comprehensive tier. No action required. -
One
webview_flutterpre-upgrade migration rule.avoid_pre_v4_webview_widget(Comprehensive, WARNING) flags construction of the removedWebViewwidget in files importingwebview_flutter, warning that the widget was deleted in v4.0.0 and the replacement isWebViewController+WebViewWidget(controller:). Report-only — the structural rewrite cannot be automated. Gated towebview_flutter < 4.0.0via thewebview_flutter_4pre-upgrade readiness pack. No action required on v4+.
Fixed #
require_timezone_displayno 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. ADateFormatbuilt solely to read its.patternstring (theDateFormat.jm(locale).patternlocale-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_expirationno longer flags a size-bounded cache that has no TTL. A capacity- ormaxSize-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 remainsavoid_unbounded_cache_growth's concern. The rule also now detects Map storage from field declarations instead of a whole-source scan, so atoMap()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 generatesanalysis_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 whileinitexcluded it. Both paths now share one filter: beta/deprecated rules stay off unless you explicitly enable them inanalysis_options_custom.yamlRULE 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_lintplugins 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_lintplugins. 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 newrule_packsblock only on aversion:line directly undersaropa_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 thesaropa_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-quotingTriggeropeners, "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 ofanalysis/paralysisso 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 inanalysis_options.yaml; there is no quick fix yet.
Changed #
require_platform_channel_cleanupupgraded from warning to error. After interprocedural cleanup tracking plus an AST-based setup check (a string literal that merely mentionssetMethodCallHandlerno longer triggers it), a MethodChannel/EventChannel handler left active pastdispose()— which fires callbacks on an unmounted widget and crashes with a setState error — now failsdart analyze.require_websocket_closeand 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 analyzeinstead of warning, because each fires only on a genuinely broken shape (a leak with no cleanup anywhere, a Driftdelete/updatewith nowhere, a missing iOS permission verified against the actualInfo.plist, a non-JSON-encodabletoJsonvalue, 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 youranalysis_options.yamlif 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 now→5m 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 frozenjust nowfrom its last paint. No action required.
Removed #
prefer_returning_shorthandsremoved — it duplicatedprefer_arrow_functions. Both flagged a block body holding a singlereturn, so the stylistic tier double-reported on the same line; the convert-to-=>quick fix moved ontoprefer_arrow_functions(which additionally covers lambdas). If you enabled the rule by name inanalysis_options.yaml, switch toprefer_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(), orsave()→_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_closeskips parent-supplied sockets. AWebSocketChannel/WebSocketfield assigned fromwidget.*(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, andrequire_database_closenow skip a resource assigned to a field or returned to the caller (closed elsewhere),require_native_resource_cleanuprecognizesArena/usingauto-free scopes and no longer demands afree()on a borrowedPointer.fromAddress, andrequire_platform_channel_cleanup/require_isolate_kill/require_websocket_closerecognize teardown delegated to a helper and ignore matching text in string literals.require_platform_channel_cleanupalso no longer re-flags a class that cleans up viaremoveMethodCallHandler. - Disposal and lifecycle rules respect ownership.
avoid_websocket_memory_leak,require_stream_subscription_cancel,require_dispose_implementation, andrequire_field_disposenow skip resources supplied by a parent (widget.controller) and recognize cleanup performed in any method, not justdispose();prefer_dispose_before_new_instanceno 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_prefsis now whole-word (pinno longer matchesshopping,spinner,mapping),require_secure_storageskips objects that are not actuallySharedPreferences,avoid_deprecated_crypto_algorithmsno longer treats thedesabbreviation (description) as DES, andrequire_secure_password_fieldinspectsobscureTextas a literal rather than matching nested children. - Memory and path rules use resolved types and word boundaries.
require_image_disposalmatches exactlyui.Image(notui.ImageFilter/Provider/Descriptor),avoid_expando_circular_referencesrequires a realExpandoand a whole-token key, andavoid_path_traversal/require_file_path_sanitizationmatch the tainting parameter as a whole identifier. - Drift, WebView, and animation detection narrowed to real sinks.
avoid_drift_update_without_wherewalks the query's own receiver chain so an unrelated.write()/.go()no longer trips it and a query carryingwhereis cleared;prefer_html_escapetargets only the HTML argument of a WebView sink;avoid_flashing_contentrequires a literalrepeat(reverse: true)with a sub-333ms duration. prefer_no_commented_out_codeno 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
metaas a direct dependency. Eight rule files importpackage:meta/meta.dart;dart pub publisherrors (exit 65) when an imported package is not independencies, which blocked the pub.dev release. Constraint is^1.18.0to 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_ifandprefer_returning_conditiongain a quick fix that returns the condition directly.if (c) return true; return false;becomesreturn c;and the if/else form becomesreturn c;/return !(c);; the condition is parenthesized when negated so operator precedence stays correct. No action required.avoid_collapsible_ifgains a quick fix that merges the nested if into its parent.if (a) { if (b) { … } }becomesif ((a) && (b)) { … }, with both conditions parenthesized to preserve precedence. No action required.prefer_null_aware_method_callsgains a quick fix that rewrites the guard with?..if (x != null) x.foo();andx != null ? x.foo() : nullboth becomex?.foo(), reusing the original receiver and arguments verbatim. No action required.avoid_classes_with_only_static_membersgains a quick fix that addsabstract finalmodifiers. This makes a static-only utility class non-instantiable, matching the existingprefer_abstract_final_static_classfix. No action required.avoid_icon_size_overrideandavoid_riverpod_string_provider_namegain a quick fix that removes the flagged named argument. Thesize:argument onIconand thename:argument on a provider are deleted together with their comma so the remaining arguments stay valid. No action required.avoid_nullable_parameters_with_default_valuesgains a quick fix that removes the redundant?. A parameter with a non-null default does not need a nullable type, soint? x = 0becomesint x = 0. No action required.- New
riverpod_2rule pack gates the Notifier-migration rule on Riverpod 2.x.prefer_notifier_over_staterecommends migratingStateProvidertoNotifierProvider, an API that only exists in Riverpod 2.0+. The new pack enables that rule only whenpubspec.lockresolvesriverpod >= 2.0.0, so a Riverpod 1.x project is never told to adopt an API it does not have. Enable it withrule_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 whenpubspec.lockresolves 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 removedDioErrortype with a quick fix toDioException;avoid_bloc_map_event_to_state(bloc >= 8.0.0) flags the removedmapEventToStateoverride;avoid_riverpod_state_notifier(riverpod >= 3.0.0) flags the legacyStateNotifier/StateNotifierProvider;avoid_go_router_legacy_redirect(go_router >= 6.0.0) flags the pre-6.0 single-argumentredirectcallback. Enable any withrule_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_statemoved out of the baseriverpodrule pack into the new gatedriverpod_2pack. If you enable rule packs withrule_packs: { enabled: [riverpod] }and want this rule, addriverpod_2to the list. The move keeps the rule from reaching Riverpod 1.x projects, where its recommendedNotifierProvidertarget does not exist. Tier-based configurations are unaffected.
Fixed #
prefer_correct_callback_field_nameno longer flags non-callback function-typed fields and parameters. Theonprefix 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 inCallback) 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_alwaysno 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 formathugs the bracket to the call paren and adds no trailing comma — so the rule's request was a false positive thatdart formatactively reverses. The argument-list exemption already covered a trailing function-expression callback; it now covers collection literals the same way, matching the SDKrequire_trailing_commasblock-formatted-last-argument exemption. Two scalar arguments with no block still require the comma. Remove any// ignore:you added for these.prefer_correct_handler_nameno longer flags boolean state getters and predicates that end in a handler suffix. The rule asks event handlers to start withon/handle, but it matched any method whose name ended in a past-tense suffix (Closed,Changed,Loaded, …) — so a state query likebool get isClosedwas wrongly told to becomeonClosed. It now skips getters entirely (a getter returns state, never handles an event) and skips boolean-predicate names prefixed withis/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_localno longer flags a receiver-less call reused into a later local. A barethis-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-thiscall 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 newreports/i18n_nllb_fallbacks.mdreport 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.mdto a short redirector pointing at the GitHubplans/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 theplans/tree owns. Doc housekeeping only. - The rule-pack lockfile resolver can now distinguish direct from transitive dependencies (
isDirectDependency), parsing thedependency:field ofpubspec.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-levelerror/warning/infoseverity 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 255group()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 viascanEmptyBodyStubTests; 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 inplans/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_checkruns the guard test in the audit phase and feedsAuditResult.stub_guard_passedintohas_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.mdso 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_looponly fires on a genuine accumulator (s = s + x). Per-element transforms —.map((e) => e + suffix), a fresh per-iteration local, aRegExp(... + ...)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_exceptionsno longer flags acatch (_)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_futureexemptsclose()/cancel()cleanup in synchronous void contexts. A controllerclose()indispose(), a subscriptioncancel()in aStreamController.onCancelclosure, and aclose()in a hand-named teardown method are recognized — awaiting is impossible there. No action required.avoid_excessive_rebuilds_animationno 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 afontSize: a.valueText wrapped in required scaffold is exempt, while a large static subtree underOpacity(opacity: a.value)still fires. No action required.prefer_setup_teardownno longer flags per-testpumpWidgetor per-group arrange. Statements bound to atestWidgetsWidgetTestercannot move tosetUp(), and when the file already declares a realsetUp()the duplicate threshold is raised so per-group arrange does not trip. No action required.prefer_single_setstateno longer merges asetStatebefore a loop with one inside the loop after an in-loopawait. Loop bodies are scanned as their own execution scope, so calls separated by a per-iteration suspension are not reported as combinable; two consecutivesetStatecalls in one iteration still are. No action required.prefer_value_listenable_builderno longer flags async-loaded, FutureBuilder-companion, or notifier-backed single-field states. A State that holds aFuture/Streamfield, assigns its field in anasyncsetState, or manually wires aListenableis exempt (aValueNotifiercannot replace those); genuine synchronous single-value display state still fires. No action required.avoid_collection_equality_checksno longer flags==/!=on model types whose class name merely starts with a collection keyword. It now uses a resolved-type check, soMapClusterModel,ListTileData, andSetupConfigare no longer mistaken forMap/List/Set; realList/Map/Setcomparisons still fire. No action required.avoid_unsafe_collection_methodsrecognizes seven more non-emptiness guard shapes before flagging.first/.last/.single. Combined== null || isEmptyandlength <= 1early returns,continue/breakguards in loops, a guard one block above the access,Map.keys/.valuesafter a guard on the map (or inside awhile (map.length > n)loop),isListNullOrEmptyextension guards, indexed targets (m[k]!.first), andsplit()results held in a variable are all treated as non-empty. Genuinely unguarded access still fires. No action required.avoid_listview_without_item_extentno longer flags lists whereitemExtentcannot help or cannot be set correctly. AshrinkWrap: truelist (any physics) is exempt because eager layout already defeats lazy extent, and aListView.builderwhose item is self-sizing (ListTilewith optional subtitle,ExpansionTile, aCommon*ListTile/PanelExpandablewrapper) is exempt because no constant extent is correct; plain fixed-height lists still fire. No action required.always_remove_listenerno longer reports a leak when add and remove reach the same listenable through different null-aware operators. The idiomaticfield!.addListener(cb)in initState paired withfield?.removeListener(cb)in dispose now matches. No action required.avoid_context_in_initstate_disposeonly fires whencontextperforms an inherited-widget or render-tree lookup. Acontextforwarded 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, anddependOnInheritedWidgetOfExactTypestill are. No action required.avoid_string_substringrecognizes more bounds guards and no longer flags provably in-bounds slices. It now accepts the else-branch of anindexOfternary, a substring evaluated inside anifcondition,isEmpty/isNotEmptyguards,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_futureno longer flagsreturn nullfrom a function declared to return a nullable Future (Future<T>?). A nullable Future explicitly permits null, so the return is type-correct; only non-nullableFuture<T>is still flagged. No action required.avoid_ios_hardcoded_device_modelno 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 ormodel.contains('iPod touch')check still fires. No action required.require_dialog_testsno longer flags calls that merely contain "Dialog" in their name. A localization string getter such asemergencyDirectoryDialogHeader(...)is not a dialog launch; the rule now matches known dialog launchers or*Dialog*calls that return an awaitable. No action required.require_error_identificationno longer flags non-color ternaries that select a log-severity enum or text label. It now requires a branch to beColor-typed, so aDebugLevels.Errorselection is not mistaken for an error-color cue. No action required.avoid_unbounded_cache_growthrecognizesremoveWhere/removeRange/clearas 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.lockchange (e.g. apub 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 withsaropaLints.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
--modeselector — gaps-only / gaps + upgrade low-quality Google→NLLB / force re-translate — with an interactive menu, persistent per-string engine provenance, and--show/--set/--unsetcommands to inspect, override, or remove cached translations. Build tooling only (excluded from the.vsix); no behavior change for users. - The translation script's
--modemenu 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 (editdictionaries.pyor 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 fromsrc/i18n/localesvia 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_PATbefore 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 opaquevsceerror and only a generic "PAT expired?" guess. No behavior change. - Documented the upstream cause of the
analyzer/analyzer_pluginversion caps inpubspec.yaml: Flutter pinsmetaexactly inside the SDK, analyzer 13 raised itsmetafloor above that pin, and the caps clear only when Flutter stable bumps its bundledmeta. 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, andavoid_clip_path_in_animated_buildercatch GPU-expensive widgets (Opacity,BackdropFilter,ShaderMask,ImageFiltered/ColorFiltered,ClipPath) placed inside anAnimatedBuilderor 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_staticno longer flags a guarded or pre-awaitBuildContextparameter. 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 amountedcheck, 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 anawaitwithout a guard. Genuinely unguarded post-await usage still flags. No action required.avoid_debug_printandavoid_print_errorno longer flag the logging infrastructure's own sink. Both rules redirect callers to structured logging, but the implementation of that logging — functions nameddebug*,_debug*, orbreadcrumb— must calldebugPrint/printdirectly, since routing back throughdebug()would recurse infinitely. Both rules now exempt calls inside those logging-primitive functions. Ordinary applicationdebugPrint/print-in-catch usage still flags. No action required.avoid_duplicate_object_elementsno longer flags a repeated entry in a gradientcolors:/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 acolors:orstops:argument. Duplicate identifiers, booleans, and nulls in ordinary collections still flag. No action required.avoid_large_list_copyno longer flags a structurally-required.toList()in a switch arm, record field, oryield. The exemption that recognizes mandatory-Listcontexts climbed through transparent wrappers but stopped at aswitch-expression arm, a record literal, or ayield, so a.toList()inside aList<T>getter built from aswitchwas wrongly flagged on every arm. It now climbs through those nodes to the enclosing return/body and treats a generatoryieldas requiring aList. A genuinely avoidable.toList()(lazy chain whose result is discarded) still flags. No action required.avoid_manual_date_formattingno longer flags non-DateTimetypes or internal keys. A string interpolation reading two or more.month/.day/.yeargetters was treated as manual date formatting even when the value came from a custom calendar type (HebrewDate, a projectDateTimewrapper) or built an internal dedup/cache key, because an unresolved type was assumed to be aDateTime. 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 realDateTimefor display still flags. No action required.avoid_missing_enum_constant_in_mapno 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_instancesandrequire_field_disposenow 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 matchesdispose()in any cascade position. A field whose cascade never calls a disposal method still flags. No action required.no_equal_argumentsno 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 exemptsfromRGBO,fromARGB,fromLTRB,Size, andOffset. A repeated identifier passed to any other callee (e.g.setPosition(x, x)) still flags. No action required.no_equal_nested_conditionsno longer flags an inner check whose variable was reassigned. When a nestedifrepeats 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_instanceno 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 anaddPostFrameCallback/Future.microtaskclosure, 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_constraintsno longer flags a window-width breakpoint query.MediaQuery.sizeOf(context).widthpassed 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 —LayoutBuilderwould 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 awidth:/height:named argument (actual widget sizing) still flags. No action required.prefer_reusing_assigned_localno 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, andValueNotifier<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_widgetno longer flags abuilder:method tear-off that handles the error. When aFutureBuilder/StreamBuilderbuilder: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 handledsnapshot.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_environmentsno longer flags environment keywords that appear only as substrings of unrelated identifiers. The rule matchedprod/release/test/dev/live/localas raw substrings, so a config class withrelease_notesandlatest(ordeveloper,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, sorelease_notes/latestare ignored while genuine mixes (apiUrlProdalongsidedebugFlag) still flag. No action required.avoid_parameter_mutationno longer flags index assignment into a List, typed-data, or Map parameter. Filling a caller-allocated buffer by index (p[i] = valueon aList/Uint8List/Mappassed 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_instancedeferred-dispose helper to walk the AST instead of substring-matchingblock.toSource(), removing asource.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_expressionscovering 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=1forces Google-only andnllb_engine.py --setupdownloads 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_shadowingno 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-lessswitchcases without ever removing them when the scope closed, so reusing the same name in a second sibling literal or a latercasewas wrongly reported. It now snapshots and restores the name set around each collection-forelement 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_disposeno longer flags a disposable local variable. The rule targets nullable instance fields, but it treated any disposedSimpleIdentifieras a non-final nullable field, so disposing a method-local (such as afinal 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_setstateno longer flagssetStatecalls in mutually-exclusive branches or across anawait. The rule counted everysetStatein a method body together, so calls in separateif/elsearms, distinctswitchcases, atrybody versus itscatch, or on opposite sides of anawait(thesetState(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 anawaitas a segment boundary, flagging only when two or moresetStatecalls share one synchronous straight-line path. Genuine sequentialsetStatecalls still flag. No action required.avoid_equal_expressionsno 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 like1024 * 1024,60 * 60,x * x, and1 << 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_localno longer flags a read re-evaluated after anawait. A suspension point lets external state change between two reads (for example aGlobalKey'scurrentContextflipping 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 interveningawaitas a barrier and stays quiet; redundant re-reads with noawaitbetween them still flag. No action required.prefer_reusing_assigned_localno longer flags an identical expression read against a shadowed inner variable. When a nested closure parameter (such as aFutureBuilderbuilder whosesnapshotshadows the outerStreamBuildersnapshot) 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_builderno longer fires on the cachedFutureBuilderidiom when the cache carries a non-Futurekey field. A non-Futurefield now counts as single-value state only when it is reassigned inside asetStatecallback, so a cache key mutated only in helper methods (or re-initialized via asetStatetear-off) is correctly ignored. Genuine single-valuesetStatestate stays flagged. No action required.prefer_value_listenable_builderno longer fires when aStatealso rebuilds via a baresetState(() {})whose callback assigns no field. A bare rebuild signals state held outside the counted fields — afinalTextEditingController/Listenableor a parent value — that a singleValueListenableBuildercannot model, so the rule now stays quiet (this also covers the commonsetState(() => cb?.call())safe-setState wrapper). Genuine single-value state still triggers. No action required.
Changed #
- Renamed the
impact_reportCLI tool toseverity_reportto match the three-level severity model (errors / warnings / info) that replaced the old five-bucket impact grades. Rundart run saropa_lints:severity_report; the olddart run saropa_lints:impact_reportkeeps 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/lowvalue set instead oferror/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_pluginconstraints 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 newermetathan Flutter stable pins, which would breakflutter 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.