Skip to content

Latest commit

 

History

History
314 lines (286 loc) · 17.6 KB

File metadata and controls

314 lines (286 loc) · 17.6 KB

1.4.0

  • include_errors now governs handle_throwing_invocations (and its _in_tests companion) as well as the two undeclared-throws rules. With the default include_errors: false, declared types assignable to dart:core's Error are 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 only AppException caught, declared, or suppressed. Previously the call-site rule ignored the key entirely, so declaring an Error — even while include_errors: false exempted that same type from being demanded at the throw site — obliged every caller to handle it. Set include_errors: true for the old behavior. A blanket @Throws({}) still reports as before: the unknown types it stands for can't be shown to be Errors. exclude_throws is unchanged and still scoped to the undeclared-throws rules.
  • The fixes and assists honor include_errors too, so they stop writing types the rules never demanded. With the default include_errors: false, Add '@Throws' to the enclosing function on a call to @Throws({AppException, StateError}) now writes @Throws({AppException}) rather than both; Wrap in 'try-on-catch', Add 'on' clauses to try, and the Narrow 'catch' assist likewise omit on StateError clauses; and none of them is offered at all for a contract whose types are all Errors. Set include_errors: true to have them written as before.

1.3.0

Breaking-ish

  • @Throws/@IgnoreThrows are now recognized only when declared by the hyper_lints package (re-exports still work). Previously any class merely named Throws activated the call-site rule — and would have silently disabled require_throws_declaration. If you vendored copies of these annotation classes instead of depending on hyper_lints, all diagnostics for them stop; switch to the real annotations (or a re-export of them).
  • @Throws on a caller is no longer accepted as propagation for a PLAINLY DISCARDED fire-and-forget future (a bare, un-awaited risky(); 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 the Add '@Throws' fix is withheld only in the discarded case.
  • handle_throwing_invocations now requires ALL of a multi-type @Throws set to be handled. Previously catching ANY one declared type silenced the whole invocation (@Throws({A, B}) with only on A {} reported nothing). Coverage may accumulate across nested trys — an inner try handling A and an outer one handling B together count as handled — and composes with declaration/suppression: types caught locally are subtracted, and only the unhandled remainder needs a @Throws or @IgnoreThrows on the containing declaration.
  • handle_throwing_invocations now also enforces @Throws on OPERATOR methods at their use sites: a + b, a[0], a[0] = v, -a, x++, and x += b produce 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 throw statements (previously @Throws was 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 @Throws at all that directly throws a non-excluded type is flagged. Never double-reports with declare_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 local try/on that 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 — bare catch and on Object catch everything, but on Error does NOT silence a thrown Exception (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 nearest analysis_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 check Error subtypes; by default anything assignable to dart:core's Error (e.g. StateError guards) is exempt, per Effective Dart's errors-are-bugs convention.
    • The section is looked up in the nearest analysis_options.yaml, following relative include: 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 nearer analysis_options.yaml needs 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({...})/@ignoreThrows on the declaration suppresses them the same way it does the call-site rule.
  • handle_throwing_invocations also 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/, and benchmark/ code (no _in_tests companion yet — that code is never checked by them). Enable the two rules as a pair: require_throws_declaration only checks that an annotation exists; completeness of a present annotation is declare_thrown_exceptions' job.
  • Only thrown types assignable to Exception or Error are checked: throw 'message' and other non-throwable objects are the SDK's only_throw_errors domain, so neither the rules nor the new fix will demand @Throws({String}).
  • throw e of a catch clause's own exception variable is now treated exactly like rethrow everywhere: it doesn't count as handling for handle_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's e from inside a nested try's catch still counts — and sees through parentheses, e!, and e as X (same object, same propagation). Conversely, both rethrow signals are containment-aware: a rethrow or throw e that a nested try provably catches again (exact subtype match) no longer marks the clause as rethrowing, removing a false-positive family that existed for rethrow since 1.2.0.
  • handle_throwing_invocations now 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_throws now excludes a listed type's subtypes as well (name-based against the supertype chain), so excluding a domain base class silences its hierarchy.
  • @Throws may now annotate setters and constructors (its @Target was narrower than what the plugin already checked, so the new fix would have produced invalid_annotation_target warnings 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 @Throws would 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 @Throws is now verified by declare_thrown_exceptions (its call sites were already enforced); unannotated local functions remain unchecked by design.

Fixed

  • Reads of a Future-valued @Throws getter (qualified or bare) are treated as synchronous invocations: the getter body runs before the future exists, so a surrounding try genuinely catches its throw and no fire-and-forget gating applies.
  • A try handles a Future-returning invocation only when an await encloses 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, while try { await Future.wait([risky()]); } on E {} stays handled).
  • A blanket @Throws({}) contract is discharged only by a universal catch (bare catch / 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 Error as catch-alls anywhere: a declared type that implements Exception can never be caught by on Error, so such call sites are now flagged. Only bare catch, on Object, and on dynamic are universal; everything else is a real subtype check.
  • Exception-flow analysis understands scope boundaries uniformly: a try around a closure or local function no longer counts as handling what runs later, an enclosing declaration's @Throws doesn't cover closure-nested invocations, throw e of 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' clauses declines when a boundary sits between the call and the try, inserts before (not after) a matching rethrowing clause, and Add '@Throws' declares only the unhandled remainder.
  • throw e as X counts as propagation only when the cast is a statically-guaranteed upcast; a failable cast is a new throw (it may produce a TypeError instead of re-throwing e).
  • The Add missing 'on' clauses fix 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/ignoreThrows reference 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 @IgnoreThrows annotations 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 when X covers 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 root analysis_options.yaml governs member packages here precisely when its plugins: section does.

Notes

  • @Throws is 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.

1.2.0

  • handle_throwing_invocations now understands rethrow: 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.

1.1.0

  • Added quick fixes for handle_throwing_invocations: wrap in try/on-catch, wrap in generic try-catch, add missing on clauses to the enclosing try, and add/merge @Throws on the enclosing function.
  • Added assists: add a template on clause to a try statement, and narrow a broad catch to the exception types declared by @Throws in the try body.
  • Try-catch fixes insert await when 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' clauses and Wrap in generic 'try-catch') offer an "everywhere in file" variant (IDE fix-all; dart fix CLI 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's Object, Exception, and Error are treated as catch-alls.
  • BREAKING-ish: minimum Dart SDK is now 3.11 (analyzer 14 / newest analyzer plugin APIs).
  • handle_throwing_invocations now also flags bare (unqualified) getter reads of a @Throws getter (riskyValue, not just obj.riskyValue) and compound-assignment reads (riskyValue += 1); a plain assignment (riskyValue = 1) is not a read and is still not flagged.
  • Added @IgnoreThrows/@ignoreThrows annotations: suppress handle_throwing_invocations for invocations inside the annotated function/method/getter/setter/field/top-level-variable/constructor. Bare @ignoreThrows suppresses every declared exception type; the typed form @IgnoreThrows({SomeException}) only suppresses invocations whose entire declared @Throws set is covered by the given set.
  • Added a new quick fix, Suppress with '@ignoreThrows': inserts a bare @ignoreThrows annotation on the enclosing declaration (adding the hyper_lints import if needed).
  • .ignore() and unawaited(...) (matched by name, so re-exports work too) on a flagged Future-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 under test/, integration_test/, test_driver/, testing/, tool/, and benchmark/ directories, so it can be toggled independently of the main rule (diagnostics: handle_throwing_invocations_in_tests: false).
  • Migration note: handle_throwing_invocations_in_tests is a separate, opt-in rule — it is OFF by default even when handle_throwing_invocations is true. After upgrading, add handle_throwing_invocations_in_tests: true to your diagnostics: config to keep flagging test/tool/benchmark code as before.
  • Performance: @Throws/@IgnoreThrows annotation lookups are now memoized per-Element via an Expando, avoiding repeated re-resolution of the same annotation across multiple call sites and rule passes.
  • analyzer_plugin is no longer a direct runtime dependency — moved to dev_dependencies (only used by this package's own tests); consumers only need analysis_server_plugin re-exports at runtime.
  • Measured performance (re-run against two real consumer projects, see benchmark/RESULTS.md): this repo's own dart test suite 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. Warm dart analyze medians 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.

1.0.6

  • Update integration example

1.0.5

  • Update readme

1.0.4

  • Update integration docs and update dart SDK constraints to minimal that supports the analyzer plugin API.

1.0.3

  • Handle async functions and Future return types in the handle_throwing_invocations lint rule, ensuring that exceptions thrown from async functions are also properly handled.
  • Bump dependencies and remove unnecessary ones.

1.0.2

  • Improve changelog docs

1.0.1

  • Rename plugin to hyper_lints to avoid conflicts with other packages that might use the same name for their plugin.

1.0.0

  • Add @Throws annotation and the corresponding lint rule handle_throwing_invocations