Release v15.0.0
Version 15.0.0 adds new quick fixes for error logging and variable placement while introducing a persistent background daemon for significantly faster IDE save-scans. This release resolves false positives across exception handling, lifecycle timers, static method detection, and platform target checks. Project tier management is now unified directly through project configuration, reducing default editor memory overhead. log
Added
require_error_loggingnow offers a quick fix: applying it inserts adebugPrintcall logging the caught error (interpolating the captured exception variable when one exists, or naming the statically-known exception type when it does not) instead of only reporting the missing log call.move_variable_closer_to_its_usagenow offers a quick fix: applying it moves the flagged declaration down to just before its first use. The fix only activates when doing so is provably safe (a single-variable declaration whose initializer shares no identifier with any statement it would move past) and otherwise leaves the diagnostic for manual review, so no action is required beyond reviewing the proposed edit before applying it.
Fixed
avoid_catching_generic_exceptionno longer flagson Object/on Exception/dynamiccatch clauses whose body forwards the caught error to a logging or crash-reporting call (or rethrows it) before falling back — this is a deliberate pattern for also catchingErrorsubtypes and reporting them, not a swallowed exception. Untypedcatch (e)is unaffected. (plans/history/2026.08/2026.08.15/avoid_catching_generic_exception_false_positive_logged_broad_catch.md)require_error_boundaryno longer flags aMaterialApp/CupertinoAppbuilt insidemain()'s owncatchclause when that clause already logged the caught error and itstrybody attemptedrunApp(...)— that's the app's crash-recovery fallback screen, not its normal entry point, and demanding it also carry an error-boundarybuilder:is recursive. The same shape outsidemain(), without logging, or without anrunAppattempt in thetrybody still requires abuilder:as before. (plans/history/2026.08/2026.08.15/require_error_boundary_false_positive_fallback_ui_inside_catch.md)require_error_loggingno longer flags acatch/on Typeclause with no captured exception variable if its body still calls a recognized logging function — a static message likeon TimeoutException { debug('timed out'); }is a complete log entry even without touching the exception object. A clause with no captured variable and no logging call is still flagged, as before. (plans/history/2026.08/2026.08.15/require_error_logging_false_positive_unparamed_catch_with_logged_body.md)require_app_lifecycle_handling,avoid_work_in_paused_state, andrequire_lifecycle_observerno longer flag aTimer/Stream.periodic/.listen()subscription that is created and canceled/closed within the sameStateclass's owninitState/dispose()pair — that's Flutter's standard cleanup contract for a foreground-only ticker that doesn't need to pause on backgrounding, since it stops existing when the widget is disposed. A class whosedispose()does not cancel the field it created, or that assigns the Timer/subscription somewhere dispose() can't prove cleanup for, is still flagged, as before. (plans/history/2026.08/2026.08.15/require_app_lifecycle_handling_false_positive_dispose_cancels_timer.md)require_ios_deployment_target_consistencyno longer flagsimport 'dart:async'(or any other import/export URI) as Swiftasync/awaitusage — the rule now skips string literals inside import/export directives before checking them against its tracked iOS 15+ API names. A genuine API name appearing elsewhere in the file is still flagged, as before. The same import/export-URI substring-match false positive was also fixed inrequire_ios_live_activities_setup(triggered byimport 'package:live_activities/...') andrequire_ios_certificate_pinning(triggered by package import paths containing segments like/auth). (plans/history/2026.08/2026.08.15/require_ios_deployment_target_consistency_false_positive_import_uri_misattribution.md)prefer_static_methodno longer flags methods that read instance fields or call instance methods via bare (unprefixed) identifiers — the idiomatic Dart style used throughout most codebases. Previously the rule only recognized an explicitthis.prefix, so any method touching instance state through a bare identifier (including inside a nested closure) was misdiagnosed as "could be static." A method that truly uses no instance state anywhere is still flagged, as before. (plans/history/2026.08/2026.08.15/prefer_static_method_false_positive_implicit_field_access.md)move_variable_closer_to_its_usageno longer flags a deliberate "load N values, then consume all N in the same order" batch shape (e.g. five sequentialawait-loads followed by five field assignments) — a sibling declaration in the same contiguous run that is itself genuinely used elsewhere, or the first-use site of another such sibling, no longer counts toward the "unrelated intervening statements" distance. A genuinely far-apart single declaration, declarations used out of matching order, or unused padding declarations sitting next to a real one, are all still flagged, as before. (plans/history/2026.08/2026.08.15/move_variable_closer_to_its_usage_false_positive_batch_declaration_grouping.md)require_firebase_app_check_productionandrequire_firebase_app_checkno longer flagFirebase.initializeApp()whenFirebaseAppCheck/AppCheckactivation is deferred to a separate, actually-called function elsewhere in the same file — a common pattern for keeping a slow/flaky Play Integrity check off the startup path. A file where App Check is only mentioned in a comment, or where the activating function exists but is never called from anywhere, is still flagged, as before. (plans/history/2026.08/2026.08.15/require_firebase_app_check_production_false_positive_activation_in_separate_function.md)require_log_level_for_productionno longer flags a bare verbose-log call (e.g.debug(...)) when the called function's own log-level parameter (level,logLevel,severity, orverbosity) already defaults to a safe value — demanding an explicitlevel:argument in that case would be a no-op. A callee whose default is itself verbose, unrecognized (numeric or constructor-call), or unresolvable, is still flagged, as before. (plans/history/2026.08/2026.08.15/require_log_level_for_production_false_positive_default_level_param.md)
Changed (Extension)
- Saving a Dart file now scans it in an external process and shows findings as squiggles and Problems panel entries — no separate setting to find or enable, this is what
saropaLints.enablednow does. Turning that toggle off stops save scans and shuts the scanner down immediately, rather than leaving stale findings in the Problems panel.saropaLints.scanOnSave.resolveTypes(default on) controls whether scans fully resolve types so type-based rules fire; turn it off only if save latency matters more than catching those rules. - Type-resolved save scans run through a persistent
scan_daemonprocess that builds the analyzer's project context once and keeps it warm, so a save is checked in a few seconds instead of re-paying a roughly one-minute analyzer warmup on every save. The status bar shows a warming message while the first scan after opening is still resolving; the daemon restarts automatically (with backoff) if it stops. Measured memory is comparable to the in-process analyzer plugin — the daemon's advantage is living outside the editor's own process, not a smaller footprint. - New projects (
dart run saropa_lints:initor the extension's Enable) no longer get a live in-process analyzer plugin — theplugins:block is written commented out by default, since it can hold several GB of resolved analysis state on large projects for no benefit over the scan-on-save daemon above. A project that already had the plugin running, or had it explicitly turned off, keeps that state through tier changes and re-enabling; uncomment the block inanalysis_options.yamlto opt back in to live in-editor squiggles, or run the new "Saropa Lints: Re-enable In-Process Plugin" command to do it in one step (it also restarts the Dart analysis server so the plugin reloads immediately). - New command "Saropa Lints: Scan Whole Project for Issues" runs a cancelable whole-project scan so files you haven't saved this session still show up in the Problems panel — save-triggered scanning alone only checks a file once you save it. It streams results in chunks as it goes and can be canceled mid-scan from the progress notification; run it from the Command Palette when you want full coverage, not automatically on open (a full pass on a large project can take tens of minutes).
- A save-triggered scan of a single file no longer prints a misleading progress bar estimating its position against the whole project (e.g. "Files: 1/4477, ETA: 2h"). That estimate now only appears during the long-lived in-editor plugin session it was designed for; one-shot scans (save-triggered daemon,
scanCLI) show a plain file count instead. analysis_options.yamlis now the single source of truth for a project's lint tier. Save-triggered scans, the whole-project baseline scan, and the tier picker's "current tier" display now read the tier straight fromanalysis_options.yamlinstead of trusting the (possibly stale)saropaLints.tiersetting, so a hand-edited or regenerated config file can no longer silently disagree with what the extension shows or scans with.SAROPA_TIERremains available as a dev-only override but now logs a warning when it disagrees with the project's own config;saropa_tier:inanalysis_options_custom.yamlis deprecated in favor ofanalysis_options.yaml.
Maintenance
- Investigated a
no_magic_stringfalse-positive report (string literal inside a//-commented-outdebugPrintcall) and confirmed by code inspection it cannot occur — the rule and all its gating helpers are AST-callback-only, with no raw-text scanning. Added a resolved-analyzer regression test pinning this behavior. (bugs/no_magic_string_false_positive_commented_out_code.md) - Manually corrected seven German and Swahili extension strings that had shipped corrupted machine-translation output — a mangled literal
--resolveCLI flag, two entries collapsed into a repetition loop (one leaking a fragment resembling a stray prompt artifact), and grammatically broken fallback text — and added each as a curateddictionaries.pyoverride so a future translation run can never regenerate the same corruption from cache.