Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
19 changes: 19 additions & 0 deletions docs/src/components/lint-rules/avoid_async_emit/BadSnippet.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Code } from '@astrojs/starlight/components';
import { transformerMetaHighlight } from '@shikijs/transformers';

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

class MyCubit extends Cubit<int> {
MyCubit() : super(0);

Future<void> loadData() async {
final data = await Future.value(42);

emit(data); // LINT
}
}
`}
lang="dart" title="my_cubit.dart"
transformers={[transformerMetaHighlight()]} class='warning' meta="{9}" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
import { Code } from '@astrojs/starlight/components';
const code = `
import 'package:flutter_bloc/flutter_bloc.dart';

class MyCubit extends Cubit<int> {
MyCubit() : super(0);

Future<void> loadData() async {
final data = await Future.value(42);

if (isClosed) return;

emit(data);
}
}
`;
---

<Code code={code} lang="dart" title="my_cubi.dart" />
40 changes: 40 additions & 0 deletions docs/src/content/docs/lint-rules/avoid_async_emit.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Avoid async emit
description: The avoid_sync_emit rule.
---

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

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

Do not use emit across asynchronous gaps.

## Rationale

DON'T use emit across asynchronous gaps.
Using emit after an asynchronous gap (such as after an await or inside an async callback) is unsafe because the Cubit or Bloc may have been closed while awaiting, leading to exceptions or unexpected behavior. Always ensure emit is called synchronously or properly guarded to avoid emitting on a closed instance.

## Examples

**Avoid** emit after async method

**BAD**:

<BadSnippet />

**GOOD**:

<GoodSnippet />

## Enable

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

<EnableRuleSnippet name="avoid_async_emit" />
1 change: 1 addition & 0 deletions packages/bloc_lint/lib/all.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
bloc:
rules:
- avoid_async_emit
- avoid_build_context_extensions
- avoid_flutter_imports
- avoid_public_bloc_methods
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 @@ -8,6 +8,7 @@ export 'src/lint_rule.dart' show LintRule, LintRuleBuilder;
export 'src/linter.dart' show LintContext, Linter;
export 'src/rules/rules.dart'
show
AvoidAsyncEmit,
AvoidBuildContextExtensions,
AvoidFlutterImports,
AvoidPublicBlocMethods,
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 @@ -24,6 +24,7 @@ final allRules = <String, LintRuleBuilder>{
PreferCubit.rule: PreferCubit.new,
PreferFileNamingConventions.rule: PreferFileNamingConventions.new,
PreferVoidPublicCubitMethods.rule: PreferVoidPublicCubitMethods.new,
AvoidAsyncEmit.rule: AvoidAsyncEmit.new,
};

/// {@template linter}
Expand Down
155 changes: 155 additions & 0 deletions packages/bloc_lint/lib/src/rules/avoid_async_emit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import 'package:bloc_lint/bloc_lint.dart';

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

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

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

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

final LintContext context;

bool _inAsyncMethod = false;
bool _isACubit = false;

int _isClosedGuardLevel = 0;
int _awaitInGuardLevel = 0;

@override
Future<void> handleAsyncModifier(Token? beginToken, Token? endToken) async {
if (!_isACubit) return;

if (beginToken?.lexeme != 'async') return;

_inAsyncMethod = true;
}

@override
void beginAwaitExpression(Token token) {
if (!_isACubit) return;
if (!_inAsyncMethod) return;

if (_isClosedGuardLevel > 0) {
_awaitInGuardLevel = _isClosedGuardLevel;
}
}

@override
void beginIfStatement(Token token) {
if (!_isACubit) return;
if (!_inAsyncMethod) return;

final next = token.next;
if (next?.lexeme == '(') {
final cond1 = next?.next;
// if (!isClosed)
if (cond1?.lexeme == '!' &&
cond1?.next?.lexeme == 'isClosed' &&
cond1?.next?.next?.lexeme == ')') {
_isClosedGuardLevel++;
}

// if (isClosed == false)
if (cond1?.lexeme == 'isClosed' &&
cond1?.next?.lexeme == '==' &&
cond1?.next?.next?.lexeme == 'false') {
_isClosedGuardLevel++;
}

// if (isClosed) return;
if (cond1?.lexeme == 'isClosed' && cond1?.next?.lexeme == ')') {
final afterParen = cond1?.next?.next;
if (afterParen?.lexeme == 'return' && afterParen?.next?.lexeme == ';') {
_isClosedGuardLevel++;
}
if (afterParen?.lexeme == '{' &&
afterParen?.next?.lexeme == 'return' &&
afterParen?.next?.next?.lexeme == ';' &&
afterParen?.next?.next?.next?.lexeme == '}') {
_isClosedGuardLevel++;
}
}

// if (isClosed == false) return;
// or if (isClosed == false) { return; }
if (cond1?.lexeme == 'isClosed' &&
cond1?.next?.lexeme == '==' &&
cond1?.next?.next?.lexeme == 'false' &&
cond1?.next?.next?.next?.lexeme == ')') {
final afterParen = cond1?.next?.next?.next?.next;
if (afterParen?.lexeme == 'return' && afterParen?.next?.lexeme == ';') {
_isClosedGuardLevel++;
}
if (afterParen?.lexeme == '{' &&
afterParen?.next?.lexeme == 'return' &&
afterParen?.next?.next?.lexeme == ';' &&
afterParen?.next?.next?.next?.lexeme == '}') {
_isClosedGuardLevel++;
}
}
}
}

@override
void endIfStatement(Token ifToken, Token? elseToken, Token endToken) {
if (!_isACubit) return;
if (!_inAsyncMethod) return;

final next = ifToken.next;
if (next?.lexeme == '(') {
final cond1 = next?.next;
if (cond1?.lexeme == '!' && cond1?.next?.lexeme == 'isClosed') {
if (_isClosedGuardLevel > 0) _isClosedGuardLevel--;
if (_awaitInGuardLevel > _isClosedGuardLevel) {
_awaitInGuardLevel = _isClosedGuardLevel;
}
}
if (cond1?.lexeme == 'isClosed' &&
cond1?.next?.lexeme == '==' &&
cond1?.next?.next?.lexeme == 'false') {
if (_isClosedGuardLevel > 0) _isClosedGuardLevel--;
if (_awaitInGuardLevel > _isClosedGuardLevel) {
_awaitInGuardLevel = _isClosedGuardLevel;
}
}
}
}

@override
void handleIdentifier(Token token, IdentifierContext _) {
final extendsACubit =
token.lexeme == 'Cubit' && token.previous?.lexeme == 'extends';

if (extendsACubit) {
_isACubit = true;
}

if (!_isACubit) return;
if (!_inAsyncMethod) return;

if (token.lexeme == 'emit') {
if (_isClosedGuardLevel > 0 && _awaitInGuardLevel < _isClosedGuardLevel) {
return;
}

context.reportToken(
token: token,
message: '''
Avoid calling emit inside async methods without guarding with isClosed.''',
hint: '''
Guard emit with if (!isClosed) or if (isClosed) return; before calling emit.''',
);
}
}
}
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,3 +1,4 @@
export 'avoid_async_emit.dart';
export 'avoid_build_context_extensions.dart';
export 'avoid_flutter_imports.dart';
export 'avoid_public_bloc_methods.dart';
Expand Down
Loading