include_errorsnow governshandle_throwing_invocations(and its_in_testscompanion) as well as the two undeclared-throws rules. With the defaultinclude_errors: false, declared types assignable todart:core'sErrorare dropped from an invocation's contract before the call site is checked:@Throws({ArgumentError})no longer reports at its callers, and@Throws({AppException, StateError})needs onlyAppExceptioncaught, declared, or suppressed. Previously the call-site rule ignored the key entirely, so declaring anError— even whileinclude_errors: falseexempted that same type from being demanded at the throw site — obliged every caller to handle it. Setinclude_errors: truefor the old behavior. A blanket@Throws({})still reports as before: the unknown types it stands for can't be shown to beErrors.exclude_throwsis unchanged and still scoped to the undeclared-throws rules.- The fixes and assists honor
include_errorstoo, so they stop writing types the rules never demanded. With the defaultinclude_errors: false,Add '@Throws' to the enclosing functionon a call to@Throws({AppException, StateError})now writes@Throws({AppException})rather than both;Wrap in 'try-on-catch',Add 'on' clauses to try, and theNarrow 'catch'assist likewise omiton StateErrorclauses; and none of them is offered at all for a contract whose types are allErrors. Setinclude_errors: trueto have them written as before.
Breaking-ish
@Throws/@IgnoreThrowsare now recognized only when declared by thehyper_lintspackage (re-exports still work). Previously any class merely namedThrowsactivated the call-site rule — and would have silently disabledrequire_throws_declaration. If you vendored copies of these annotation classes instead of depending onhyper_lints, all diagnostics for them stop; switch to the real annotations (or a re-export of them).@Throwson a caller is no longer accepted as propagation for a PLAINLY DISCARDED fire-and-forget future (a bare, un-awaitedrisky();statement): its error never reaches the caller's own future. Futures that are stored, passed as arguments (e.g.Future.wait([...])), returned, or awaited later keep propagating as before, and theAdd '@Throws'fix is withheld only in the discarded case.handle_throwing_invocationsnow requires ALL of a multi-type@Throwsset to be handled. Previously catching ANY one declared type silenced the whole invocation (@Throws({A, B})with onlyon A {}reported nothing). Coverage may accumulate across nested trys — an inner try handlingAand an outer one handlingBtogether count as handled — and composes with declaration/suppression: types caught locally are subtracted, and only the unhandled remainder needs a@Throwsor@IgnoreThrowson the containing declaration.handle_throwing_invocationsnow also enforces@Throwson OPERATOR methods at their use sites:a + b,a[0],a[0] = v,-a,x++, andx += bproduce diagnostics that did not exist in 1.2.0 when the resolved operator (or an involved accessor) declares@Throws. A compound form reports ONE diagnostic carrying the union of the getter/setter/operator contracts.
Added
- Added two opt-in rules checking a function's OWN
throwstatements (previously@Throwswas entirely on the honor system — only call sites were checked):declare_thrown_exceptions: a@Throws-annotated function that directly throws an escaping type its declared set doesn't cover is flagged at the throw expression. Subtypes of a declared type count as covered;@Throws({})is treated as a blanket declaration.require_throws_declaration: strict mode — a function with no@Throwsat all that directly throws a non-excluded type is flagged. Never double-reports withdeclare_thrown_exceptions(one requires the annotation, the other its absence).
- Escape analysis is rethrow-aware and follows first-matching-clause
dispatch (like
handle_throwing_invocations): a localtry/onthat catches the type silences the rules; a matching clause that rethrows does not. Because the thrown type is statically exact, clause matching is a real subtype check — barecatchandon Objectcatch everything, buton Errordoes NOT silence a thrownException(or vice versa), since it wouldn't catch it at runtime. Throws inside closures and local functions don't count against the enclosing function. - New custom configuration section (top-level
hyper_lints:key in the nearestanalysis_options.yaml— the analyzer's plugin config schema only supports per-rule on/off):exclude_throws: [TypeName, ...]— class names never required in@Throws(name-based matching).include_errors: true— also checkErrorsubtypes; by default anything assignable todart:core'sError(e.g.StateErrorguards) is exempt, per Effective Dart's errors-are-bugs convention.- The section is looked up in the nearest
analysis_options.yaml, following relativeinclude:chains (nearest section wins; a later include beats an earlier one).package:includes are not resolved. Edits to any file in the chain (and deleting the options file, or creating a previously-missing include target) take effect on the next analysis; only creating a brand-new neareranalysis_options.yamlneeds an analysis-server restart.
- New quick fix for both rules: Declare the thrown type in '@Throws'
(creates or merges the annotation). Suppress with '@ignoreThrows'
is offered for them too;
@IgnoreThrows({...})/@ignoreThrowson the declaration suppresses them the same way it does the call-site rule. handle_throwing_invocationsalso checks increment/decrement (x++,--x) on annotated accessors, and an assignment/increment hitting BOTH an annotated getter and setter carries the union of the two contracts in one diagnostic — the fixes compute the same union, so applying one can't strand the other accessor's diagnostic.- Both new rules skip
test/,integration_test/,test_driver/,testing/,tool/, andbenchmark/code (no_in_testscompanion yet — that code is never checked by them). Enable the two rules as a pair:require_throws_declarationonly checks that an annotation exists; completeness of a present annotation isdeclare_thrown_exceptions' job. - Only thrown types assignable to
ExceptionorErrorare checked:throw 'message'and other non-throwable objects are the SDK'sonly_throw_errorsdomain, so neither the rules nor the new fix will demand@Throws({String}). throw eof a catch clause's own exception variable is now treated exactly likerethroweverywhere: it doesn't count as handling forhandle_throwing_invocations(on X catch (e) { throw e; }no longer silences the call-site rule), and the undeclared-throws rules treat it as propagation rather than a new throw. Matching is by element identity and works at any nesting depth — re-throwing the outer clause'sefrom inside a nested try's catch still counts — and sees through parentheses,e!, ande as X(same object, same propagation). Conversely, both rethrow signals are containment-aware: arethroworthrow ethat a nested try provably catches again (exact subtype match) no longer marks the clause as rethrowing, removing a false-positive family that existed forrethrowsince 1.2.0.handle_throwing_invocationsnow also checks setter writes: an assignment to a@Throws-annotated setter (riskyValue = 1,obj.riskyValue = 1) is an invocation and is flagged unless handled or declared; the fixes are offered there too. A compound assignment hitting both an annotated getter and an annotated setter reports once.exclude_throwsnow excludes a listed type's subtypes as well (name-based against the supertype chain), so excluding a domain base class silences its hierarchy.@Throwsmay now annotate setters and constructors (its@Targetwas narrower than what the plugin already checked, so the new fix would have producedinvalid_annotation_targetwarnings there).- The undeclared-throws rules skip generator bodies (
sync*/async*): their throws surface on iteration, where no try around the call can catch them, so demanding@Throwswould mislead callers. - The call-site rule's propagation check and the
Add '@Throws'fix now accept constructors as the containing declaration (previously a@Throws-annotated constructor calling a throwing function was still flagged, and the fix wasn't offered inside constructors). - A local function that itself carries
@Throwsis now verified bydeclare_thrown_exceptions(its call sites were already enforced); unannotated local functions remain unchecked by design.
Fixed
- Reads of a
Future-valued@Throwsgetter (qualified or bare) are treated as synchronous invocations: the getter body runs before the future exists, so a surroundingtrygenuinely catches its throw and no fire-and-forget gating applies. - A
tryhandles a Future-returning invocation only when anawaitencloses the invocation inside that try's body: a stored or passed future's failure happens after the frame returned, so the handler is unreachable (try { final f = risky(); } on E {}flags again, whiletry { await Future.wait([risky()]); } on E {}stays handled). - A blanket
@Throws({})contract is discharged only by a universal catch (barecatch/on Object/on dynamic): a typed clause says nothing about the arbitrary exceptions an unknown contract may produce. - Catch-clause matching no longer treats
on Exception/on Erroras catch-alls anywhere: a declared type that implementsExceptioncan never be caught byon Error, so such call sites are now flagged. Only barecatch,on Object, andon dynamicare universal; everything else is a real subtype check. - Exception-flow analysis understands scope boundaries uniformly: a
tryaround a closure or local function no longer counts as handling what runs later, an enclosing declaration's@Throwsdoesn't cover closure-nested invocations,throw eof a captured clause variable inside a closure/local function is that function's own (deferred) throw — and an immediately invoked SYNCHRONOUS closure ((() { ... })()) is transparent in both directions, while async and generator IIFEs keep ordinary closure semantics. The fixes respect the same boundaries:Add missing 'on' clausesdeclines when a boundary sits between the call and the try, inserts before (not after) a matching rethrowing clause, andAdd '@Throws'declares only the unhandled remainder. throw e as Xcounts as propagation only when the cast is a statically-guaranteed upcast; a failable cast is a new throw (it may produce aTypeErrorinstead of re-throwinge).- The
Add missing 'on' clausesfix subtracts coverage from every enclosing try, so it no longer inserts a TODO stub that would shadow an outer handler; the narrow-catch assist now also sees setter writes, increments, and bare getter reads. - When an unprefixed
Throws/ignoreThrowsreference would collide with a foreign name in scope, the insert fixes import hyper_lints under a prefix (@hyper_lints.Throws({...})) instead of producing ambiguous code or no edit. - Multiple
@IgnoreThrowsannotations on one declaration now aggregate (any bare form suppresses everything, typed sets union). Previously only the first was read, which made the suppress quick fix a silent no-op on a declaration already carrying a typed, non-covering set. - The rethrow analysis threads the precise declared/thrown types into
containment checks, so a bare
catch (e) { try { throw e; } on X {} }is correctly seen as handled whenXcovers the declared types. - The
hyper_lints:config walk searches ancestor directories exactly like the analyzer's own options lookup (no package-root stop), so a monorepo's rootanalysis_options.yamlgoverns member packages here precisely when itsplugins:section does.
Notes
@Throwsis per-declaration, not inherited: overrides must re-declare (documented and pinned; call sites resolve statically, so the contract must sit on every static target).- Rule messages print the full thrown type including type arguments,
matching what the fix writes into
@Throws.
handle_throwing_invocationsnow understandsrethrow: a catch clause that rethrows (e.g.on StateError { rethrow; }, or logs and then rethrows) no longer counts as handling the exception, since the exception escapes the try statement. Such invocations are flagged again unless an outer try catches the type or the containing function declares@Throws. Catch-clause matching also now respects Dart's first-matching-clause dispatch: a broader clause after a rethrowing one can't rescue a type the rethrowing clause already catches.
- Added quick fixes for
handle_throwing_invocations: wrap in try/on-catch, wrap in generic try-catch, add missingonclauses to the enclosing try, and add/merge@Throwson the enclosing function. - Added assists: add a template
onclause to a try statement, and narrow a broadcatchto the exception types declared by@Throwsin the try body. - Try-catch fixes insert
awaitwhen the enclosing body is async, so the generated handler actually catches async exceptions. - Wrapping a declaration whose variable is used later now splits it into a
nullable declaration before the try (
int? x;), keeping later uses in scope. - The two wrap fixes (
Wrap in 'try' with 'on' clausesandWrap in generic 'try-catch') offer an "everywhere in file" variant (IDE fix-all;dart fixCLI support is still pending upstream). - The narrow-catch assist no longer suggests types already handled by nested try statements.
- Exception-type matching now uses real subtype checks; only
dart:core'sObject,Exception, andErrorare treated as catch-alls. - BREAKING-ish: minimum Dart SDK is now 3.11 (analyzer 14 / newest analyzer plugin APIs).
handle_throwing_invocationsnow also flags bare (unqualified) getter reads of a@Throwsgetter (riskyValue, not justobj.riskyValue) and compound-assignment reads (riskyValue += 1); a plain assignment (riskyValue = 1) is not a read and is still not flagged.- Added
@IgnoreThrows/@ignoreThrowsannotations: suppresshandle_throwing_invocationsfor invocations inside the annotated function/method/getter/setter/field/top-level-variable/constructor. Bare@ignoreThrowssuppresses every declared exception type; the typed form@IgnoreThrows({SomeException})only suppresses invocations whose entire declared@Throwsset is covered by the given set. - Added a new quick fix, Suppress with '@ignoreThrows': inserts a bare
@ignoreThrowsannotation on the enclosing declaration (adding thehyper_lintsimport if needed). .ignore()andunawaited(...)(matched by name, so re-exports work too) on a flaggedFuture-returning call now count as handled, including through a.then()/.whenComplete()/.timeout()chain.- Added
handle_throwing_invocations_in_tests: the same rule reported under its own diagnostic code for code undertest/,integration_test/,test_driver/,testing/,tool/, andbenchmark/directories, so it can be toggled independently of the main rule (diagnostics: handle_throwing_invocations_in_tests: false). - Migration note:
handle_throwing_invocations_in_testsis a separate, opt-in rule — it is OFF by default even whenhandle_throwing_invocationsistrue. After upgrading, addhandle_throwing_invocations_in_tests: trueto yourdiagnostics:config to keep flagging test/tool/benchmark code as before. - Performance:
@Throws/@IgnoreThrowsannotation lookups are now memoized per-Elementvia anExpando, avoiding repeated re-resolution of the same annotation across multiple call sites and rule passes. analyzer_pluginis no longer a direct runtime dependency — moved todev_dependencies(only used by this package's own tests); consumers only needanalysis_server_pluginre-exports at runtime.- Measured performance (re-run against two real consumer projects, see
benchmark/RESULTS.md): this repo's owndart testsuite is ~16-55% faster wall-clock (two consecutive runs: 20212ms→9149ms, 12947ms→10828ms), primarily from a shared analysis-context test harness and the new memoization. Warmdart analyzemedians against two external projects moved +13.5%/+14.3% (vision_kit, server) in this measurement — expected, since the rule now visits strictly more AST node kinds per file (bare getters, compound assignments) than the pre-1.1.0 baseline it's compared against; that comparison is net-of-added-coverage, not a like-for-like perf measurement of the optimizations alone.
- Update integration example
- Update readme
- Update integration docs and update dart SDK constraints to minimal that supports the analyzer plugin API.
- Handle async functions and Future return types in the
handle_throwing_invocationslint rule, ensuring that exceptions thrown from async functions are also properly handled. - Bump dependencies and remove unnecessary ones.
- Improve changelog docs
- Rename plugin to
hyper_lintsto avoid conflicts with other packages that might use the same name for their plugin.
- Add @Throws annotation and the corresponding lint rule
handle_throwing_invocations