Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Annotations

A comprehensive Dart analyzer plugin that provides powerful annotations and static analysis rules for them.

Annotations

  • @Throws: Declare the exceptions that a function can throw, enabling better documentation and static analysis of error handling.
  • @IgnoreThrows / @ignoreThrows: Suppress handle_throwing_invocations (and its test-directory companion) for invocations inside the annotated declaration. See Quick Fixes & Assists below.

Rules

  • handle_throwing_invocations: Ensures that any function that calls a function annotated with @Throws either catches the declared exceptions or also declares them with @Throws.
  • 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. See Configuration.

Quick Fixes & Assists

When handle_throwing_invocations reports an unhandled invocation, your IDE (IntelliJ/Android Studio, VS Code — anything speaking to the Dart Analysis Server) offers these quick fixes:

  • Wrap in 'try' with an 'on' clause per declared exception
  • Wrap in generic 'try-catch'
  • Add missing 'on' clauses to the enclosing 'try' — when the call is already inside a try that doesn't cover the declared types
  • Add '@Throws' to the enclosing function — propagate instead of handle; merges into an existing @Throws set
  • Suppress with '@ignoreThrows' — inserts a bare @ignoreThrows annotation on the enclosing function/method/constructor/top-level variable/field declaration (adding the hyper_lints import if needed), silencing the diagnostic by suppression instead of handling or propagating it

@IgnoreThrows / @ignoreThrows

Annotate a function, method, getter, setter, field, top-level variable, or constructor to suppress handle_throwing_invocations for invocations inside it:

@Throws({FormatException})
void parseData(String input) { /* may throw FormatException */ }

@ignoreThrows // bare form: suppresses every declared exception type
void callerThatAcceptsAnyRisk() {
  parseData('...'); // not flagged
}

@IgnoreThrows({FormatException}) // typed form: only these types
void callerThatAcceptsFormatExceptionOnly() {
  parseData('...'); // not flagged: FormatException is covered
}

The typed set only suppresses invocations whose entire declared @Throws set is covered by it — a call declaring a type outside the set still lints. ignoreThrows is shorthand for the bare IgnoreThrows() constructor.

Coverage notes

  • The rule also flags bare (unqualified) getter reads — e.g. a top-level or local @Throws getter read as plain riskyValue, not just obj.riskyValue — and compound-assignment reads, e.g. riskyValue += 1 (a plain riskyValue = 1 assignment doesn't read the getter, so it isn't flagged).
  • .ignore() and unawaited(...) (matched by name, so re-exports work too) on a flagged Future-returning call are treated as handled, including through a .then()/.whenComplete()/.timeout() chain, e.g. unawaited(risky().then((_) {}));.

Two assists are available on any try statement (no diagnostic needed):

  • Add 'on' clause — inserts a template on Exception catch (e) clause
  • Narrow 'catch' to declared exception types — when a broad catch swallows specific @Throws types thrown inside the try body, inserts specific on clauses above it

Fixes are async- and scope-aware:

  • await insertion — when the flagged call returns a Future and the enclosing function body is async, the try-catch fixes insert await so the handler actually catches. In a sync body, an un-awaited async call throws after the try/catch has already returned, so no wrap fix or Add missing 'on' clauses can ever silence the diagnostic for it — those fixes aren't offered for un-awaited async calls in sync bodies (await it, or see the SDK's unawaited_futures lint); Add '@Throws' remains available since it silences by propagation instead of catching.
  • Declaration splitting — wrapping final x = risky(); when x is used later splits the declaration out of the try as a nullable variable (int? x;), keeping later code in scope. Later uses may need ! at typed use sites; the fix does not rewrite them.
  • Apply in file — the two wrap fixes (Wrap in 'try' with 'on' clauses and Wrap in generic 'try-catch') offer an "everywhere in file" variant in the IDE. (dart fix on the command line cannot apply plugin fixes yet; see dart-lang/sdk#53402.)
  • Narrowing is nesting-aware — the narrow-catch assist ignores exception types already handled by nested try statements.
  • Type matching uses real subtype checks; only dart:core's Object/Exception/Error act as catch-alls.

After upgrading the plugin, restart the Dart Analysis Server (IntelliJ: Dart Analysis tool window → restart icon) to pick up the fixes.

Installation

Requires Dart 3.11+ (Flutter with Dart 3.11+).

Add this package as a dependency:

dependencies:
  hyper_lints: ^1.1.0

Configuration

You can configure it in your analysis_options.yaml. Both rules are opt-in: handle_throwing_invocations and its companion handle_throwing_invocations_in_tests are each OFF by default and only take effect once explicitly listed as true in the diagnostics: map below — listing one does not enable the other.

plugins:
  hyper_lints:
    version: ^1.1.0
    diagnostics:
      handle_throwing_invocations: true
      # Set to `false` (or omit this line entirely) to silence `test/`,
      # `tool/`, `benchmark/`, and `integration_test/` code instead of
      # flagging it.
      handle_throwing_invocations_in_tests: true

handle_throwing_invocations_in_tests reports the same problem under its own diagnostic code for code under test/, integration_test/, test_driver/, testing/, tool/, and benchmark/ — it has its own on/off switch, so you can enable the rule in your main code while disabling it for tests (or vice versa):

diagnostics:
  handle_throwing_invocations: true
  handle_throwing_invocations_in_tests: false

Migrating from an earlier version: if your existing config lists only handle_throwing_invocations: true, it will keep flagging lib/ code but will no longer flag test/, tool/, benchmark/, or integration_test/ code after upgrading, since handle_throwing_invocations_in_tests is a separate, independently opt-in rule rather than something the main rule's true also implies. Add handle_throwing_invocations_in_tests: true to your config to keep flagging that code too.

Usage

Basic Usage

@Throws({CustomException})
void riskyFunction() { /* ... */ }

// ✅ Specific exception type
try {
  riskyFunction();
} on CustomException catch (e) {
  // handle
}

// ✅ General Exception catch
try {
  riskyFunction();
} on Exception catch (e) {
  // handle
}

// ✅ Catch-all
try {
  riskyFunction();
} catch (e) {
  // handle
}

// ✅ Rethrowing with @Throws
@Throws({CustomException})
void callerFunction() {
  riskyFunction(); // OK because caller also declares @Throws
}

// ❌ Not declaring @Throws in caller
void anotherCallerFunction() {
    riskyFunction(); // Warning: callerFunction should declare @Throws
}

// ❌ Wrong exception type caught
try {
  riskyFunction();
} on StateError catch (e) {
  // This doesn't catch CustomException!
  // Warning: Unhandled exception from invocation annotated with @Throws
}

// ❌ Rethrowing catch clause
try {
  riskyFunction();
} on CustomException {
  rethrow; // The exception still escapes (even after logging first)!
  // Warning: Unhandled exception from invocation annotated with @Throws
  // Catch it in an outer try, or declare @Throws on the enclosing function.
}

Async Functions

@Throws({CustomException})
Future<void> riskyAsyncFunction() async { /* ... */ }

// ✅ Awaited call inside try-catch
try {
  await riskyAsyncFunction();
} catch (e) {
  // handle
}

// ✅ Using .catchError()
riskyAsyncFunction().catchError((e) {
  // handle
});

// ✅ Using .then() with onError
riskyAsyncFunction().then((_) {
  // success
}, onError: (e) {
  // handle
});

// ✅ Chained .then().catchError()
riskyAsyncFunction()
  .then((_) => print('success'))
  .catchError((e) => print('error'));

// ❌ Non-awaited call - try-catch won't catch async exceptions!
try {
  riskyAsyncFunction(); // Warning: async call not awaited
} catch (e) {
  // This won't catch the exception!
}

// ❌ Unhandled async call
riskyAsyncFunction(); // Warning: Unhandled exception

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

A set of powerful lints used by Hyperdesigned products

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages