Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Code } from '@astrojs/starlight/components';
import { transformerMetaHighlight } from '@shikijs/transformers';

<Code
code={`
import 'package:bloc/bloc.dart';

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {
CounterIncrementPressed(this.amount);

// Avoid mutable events!
// Prefer marking fields as final.
int amount;
}
`}
lang="dart" title="counter_event.dart"
transformers={[transformerMetaHighlight()]} class='warning' meta="{10}"
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { Code } from '@astrojs/starlight/components';

const code = `
import 'package:bloc/bloc.dart';

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {
const CounterIncrementPressed(this.amount);

final int amount;
}
`;
---

<Code code={code} lang="dart" title="counter_event.dart" />
50 changes: 50 additions & 0 deletions docs/src/content/docs/lint-rules/avoid_mutable_events.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Avoid Mutable Events
description: The avoid_mutable_events rule.
---

import { Badge } from '@astrojs/starlight/components';
import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro';
import BadSnippet from '~/components/lint-rules/avoid_mutable_events/BadSnippet.mdx';
import GoodSnippet from '~/components/lint-rules/avoid_mutable_events/GoodSnippet.astro';

<div class="badges">
<Badge text="new" />
<Badge text="dart" variant="note" />
</div>

Avoid mutable fields and setters on `Bloc` events.

## Rationale

Events are not handled the moment they are added. They are queued and processed
asynchronously, and an `EventTransformer` such as `debounce` or `throttle` can
delay them further. If an event is mutated after being added, the handler
observes different data than the caller intended, and the resulting behavior
depends on timing.

Mutable fields also break value equality, which blocs and tests rely on when
comparing events — for example when extending `Equatable` or when a transformer
compares incoming events.

Modeling events as immutable value objects keeps them a faithful record of what
happened.

## Examples

**Avoid** mutable fields and setters on events.

**BAD**:

<BadSnippet />

**GOOD**:

<GoodSnippet />

## Enable

To enable the `avoid_mutable_events` rule, add it to your
`analysis_options.yaml` under `bloc` > `rules`:

<EnableRuleSnippet name="avoid_mutable_events" />
1 change: 1 addition & 0 deletions packages/bloc_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ For more information, check out the [official documentation](https://bloclibrary

- [avoid_build_context_extensions](https://bloclibrary.dev/lint-rules/avoid_build_context_extensions)
- [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports)
- [avoid_mutable_events](https://bloclibrary.dev/lint-rules/avoid_mutable_events)
- [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods)
- [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields)
- [prefer_bloc](https://bloclibrary.dev/lint-rules/prefer_bloc)
Expand Down
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bloc:
rules:
- avoid_build_context_extensions
- avoid_flutter_imports
- avoid_mutable_events
- avoid_public_bloc_methods
- avoid_public_fields
- prefer_bloc
Expand Down
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/bloc_lint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/rules/rules.dart'
show
AvoidBuildContextExtensions,
AvoidFlutterImports,
AvoidMutableEvents,
AvoidPublicBlocMethods,
AvoidPublicFields,
PreferBloc,
Expand Down
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/src/linter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:path/path.dart' as p;
final allRules = <String, LintRuleBuilder>{
AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new,
AvoidFlutterImports.rule: AvoidFlutterImports.new,
AvoidMutableEvents.rule: AvoidMutableEvents.new,
AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new,
AvoidPublicFields.rule: AvoidPublicFields.new,
PreferBloc.rule: PreferBloc.new,
Expand Down
122 changes: 122 additions & 0 deletions packages/bloc_lint/lib/src/rules/avoid_mutable_events.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'package:bloc_lint/bloc_lint.dart';

/// {@template avoid_mutable_events}
/// The avoid_mutable_events lint rule.
/// {@endtemplate}
class AvoidMutableEvents extends LintRule {
/// {@macro avoid_mutable_events}
AvoidMutableEvents([Severity? severity])
: super(name: rule, severity: severity ?? Severity.warning);

/// The name of the lint rule.
static const rule = 'avoid_mutable_events';

@override
Listener? create(LintContext context) => _Listener(context);
}

class _Listener extends Listener {
_Listener(this.context);

final LintContext context;

/// Whether the enclosing class is a bloc event.
bool _isEventClass = false;

@override
void beginClassDeclaration(
Token begin,
Token? abstractToken,
Token? sealedToken,
Token? baseToken,
Token? interfaceToken,
Token? finalToken,
Token? augmentToken,
Token? mixinToken,
Token name,
) {
// e.g. `sealed class CounterEvent {}`
_isEventClass = name.lexeme.isEventType;
}

@override
void handleClassExtends(Token? extendsKeyword, int typeCount) {
final superclass = extendsKeyword?.next;
if (superclass == null) return;
// e.g. `final class CounterIncrementPressed extends CounterEvent {}`
if (superclass.lexeme.isEventType) _isEventClass = true;
}

@override
void endClassDeclaration(Token beginToken, Token endToken) {
_isEventClass = false;
}

@override
void endFields(
DeclarationKind kind,
Token? abstractToken,
Token? augmentToken,
Token? externalToken,
Token? staticToken,
Token? covariantToken,
Token? lateToken,
Token? varFinalOrConst,
int count,
Token beginToken,
Token endToken,
) {
if (!_isEventClass) return;
if (kind != DeclarationKind.Class) return;

// Static fields are not part of the event instance.
if (staticToken != null) return;

// `final` and `const` fields cannot be reassigned.
if (varFinalOrConst != null && !varFinalOrConst.isVar) return;

context.reportTokenRange(
beginToken: beginToken,
endToken: endToken,
message: 'Avoid mutable events.',
hint: 'Prefer marking fields as final.',
);
}

@override
void beginMethod(
DeclarationKind declarationKind,
Token? augmentToken,
Token? externalToken,
Token? staticToken,
Token? covariantToken,
Token? varFinalOrConst,
Token? getOrSet,
Token name,
String? enclosingDeclarationName,
) {
if (!_isEventClass) return;
if (declarationKind != DeclarationKind.Class) return;
if (staticToken != null) return;
if (getOrSet == null || !getOrSet.isSet) return;

context.reportToken(
token: name,
message: 'Avoid mutable events.',
hint: 'Prefer removing the setter.',
);
}
}

extension on String {
/// Whether the type name refers to a bloc event.
bool get isEventType => endsWith('Event');
}

extension on Token {
/// Whether the token is the `var` keyword.
bool get isVar => type == Keyword.VAR;

/// Whether the token is the `set` keyword.
bool get isSet => type == Keyword.SET;
}
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/src/rules/rules.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'avoid_build_context_extensions.dart';
export 'avoid_flutter_imports.dart';
export 'avoid_mutable_events.dart';
export 'avoid_public_bloc_methods.dart';
export 'avoid_public_fields.dart';
export 'prefer_bloc.dart';
Expand Down
Loading