Repository: felangel/bloc (dart)

Issue:
feat(bloc_lint): avoid_context_extensions
**Description**

As a developer, I want to be warned if I am using context extensions (e.g. `context.read`, `context.watch`) (originally referenced in #4264)

Pull request:
## Status

**READY**

## Breaking Changes

NO

## Description

As `avoid_context_extensions` lint rule to warn when using context extensions (e.g. context.read, context.watch)

## Related Issue

- https://github.com/felangel/bloc/issues/4457

## Type of Change

- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/docs/src/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx b/docs/src/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx
new file mode 100644
index 00000000000..47ad28ae89d
--- /dev/null
+++ b/docs/src/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx
@@ -0,0 +1,32 @@
+import { Code } from '@astrojs/starlight/components';
+import { transformerMetaHighlight } from '@shikijs/transformers';
+
+<Code
+	code={`
+  import 'package:flutter/material.dart';
+  import 'package:flutter_bloc/flutter_bloc.dart';
+
+  class CounterPage extends StatelessWidget {
+    const CounterPage({super.key});
+
+    @override
+    Widget build(BuildContext context) {
+      final count = context.watch<CounterBloc>().state; // LINT
+
+      return Column(
+        children: [
+          Text('Count: $count'),
+          ElevatedButton(
+            onPressed: () {
+              context.read<CounterBloc>().add(CounterIncrement()); // LINT
+            },
+            child: const Text('Increment'),
+          ),
+        ],
+      );
+    }
+  }
+`}
+lang="dart" title="counter_page.dart"
+transformers={[transformerMetaHighlight()]} class='warning' meta="{9,16}"
+/> 
diff --git a/docs/src/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro b/docs/src/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro
new file mode 100644
index 00000000000..f14d437562e
--- /dev/null
+++ b/docs/src/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro
@@ -0,0 +1,31 @@
+---
+import { Code } from '@astrojs/starlight/components';
+
+const code = `
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class CounterPage extends StatelessWidget {
+  const CounterPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        BlocBuilder<CounterBloc, int>(
+          builder: (context, state) => Text('Count: \$state'),
+        ),
+        ElevatedButton(
+          onPressed: () {
+            BlocProvider.of<CounterBloc>(context).add(CounterIncrement());
+          },
+          child: const Text('Increment'),
+        ),
+      ],
+    );
+  }
+}
+`;
+---
+
+<Code code={code} lang="dart" title="counter_page.dart" />
diff --git a/docs/src/content/docs/lint-rules/avoid_build_context_extensions.mdx b/docs/src/content/docs/lint-rules/avoid_build_context_extensions.mdx
new file mode 100644
index 00000000000..034f01b3b7a
--- /dev/null
+++ b/docs/src/content/docs/lint-rules/avoid_build_context_extensions.mdx
@@ -0,0 +1,55 @@
+---
+title: Avoid BuildContext Extensions
+description: The avoid_build_context_extensions rule.
+---
+
+import { Badge } from '@astrojs/starlight/components';
+import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro';
+import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx';
+import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
+
+<div class="badges">
+	<Badge text="new" />
+	<Badge text="dart" variant="note" />
+</div>
+
+Avoid using `BuildContext` extensions to access `Bloc` or `Cubit` instances.
+
+:::note
+
+This lint rule was introduced in version `0.3.0` of
+[`package:bloc_lint`](https://pub.dev/packages/bloc_lint)
+
+:::
+
+## Rationale
+
+For consistency and for the sake of being explicit, prefer directly using the
+underlying methods instead of `BuildContext` extensions. This is also beneficial
+for testing because it is not possible to mock an extension method.
+
+| extension        | explicit method                                                     |
+| ---------------- | ------------------------------------------------------------------- |
+| `context.read`   | `BlocProvider.of<Bloc>(context, listen: false)`                     |
+| `context.watch`  | `BlocBuilder<Bloc, State>(...)` or `BlocProvider.of<Bloc>(context)` |
+| `context.select` | `BlocSelector<Bloc, State>(...)`                                    |
+
+## Examples
+
+**Avoid** using `BuildContext` extensions to interact with `Bloc` or `Cubit`
+instances.
+
+**BAD**:
+
+<BadSnippet />
+
+**GOOD**:
+
+<GoodSnippet />
+
+## Enable
+
+To enable the `avoid_build_context_extensions` rule, add it to your
+`analysis_options.yaml` under `bloc` > `rules`:
+
+<EnableRuleSnippet name="avoid_build_context_extensions" />
diff --git a/packages/bloc_lint/lib/all.yaml b/packages/bloc_lint/lib/all.yaml
index 164b7677026..4e49f57b6a6 100644
--- a/packages/bloc_lint/lib/all.yaml
+++ b/packages/bloc_lint/lib/all.yaml
@@ -1,5 +1,6 @@
 bloc:
   rules:
+    - avoid_build_context_extensions
     - avoid_flutter_imports
     - avoid_public_bloc_methods
     - avoid_public_fields
diff --git a/packages/bloc_lint/lib/bloc_lint.dart b/packages/bloc_lint/lib/bloc_lint.dart
index b14ffa76de2..44743626735 100644
--- a/packages/bloc_lint/lib/bloc_lint.dart
+++ b/packages/bloc_lint/lib/bloc_lint.dart
@@ -1,5 +1,5 @@
 export 'package:_fe_analyzer_shared/src/parser/parser.dart'
-    show DeclarationKind, Listener;
+    show DeclarationKind, IdentifierContext, Listener;
 export 'package:_fe_analyzer_shared/src/scanner/token.dart'
     show BeginToken, Keyword, Token, TokenType;
 
@@ -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
+        AvoidBuildContextExtensions,
         AvoidFlutterImports,
         AvoidPublicBlocMethods,
         AvoidPublicFields,
diff --git a/packages/bloc_lint/lib/src/linter.dart b/packages/bloc_lint/lib/src/linter.dart
index 9ed3c223853..5b899bd390f 100644
--- a/packages/bloc_lint/lib/src/linter.dart
+++ b/packages/bloc_lint/lib/src/linter.dart
@@ -16,6 +16,7 @@ import 'package:path/path.dart' as p;
 
 /// All supported lint rules.
 final allRules = <String, LintRuleBuilder>{
+  AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new,
   AvoidFlutterImports.rule: AvoidFlutterImports.new,
   AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new,
   AvoidPublicFields.rule: AvoidPublicFields.new,
@@ -204,7 +205,7 @@ class LintContext {
     report(
       range: Range(
         start: document.positionAt(beginToken.offset),
-        end: document.positionAt(endToken.offset),
+        end: document.positionAt(endToken.offset + endToken.length),
       ),
       message: message,
       hint: hint,
diff --git a/packages/bloc_lint/lib/src/rules/avoid_build_context_extensions.dart b/packages/bloc_lint/lib/src/rules/avoid_build_context_extensions.dart
new file mode 100644
index 00000000000..fe839253f2e
--- /dev/null
+++ b/packages/bloc_lint/lib/src/rules/avoid_build_context_extensions.dart
@@ -0,0 +1,108 @@
+import 'package:bloc_lint/bloc_lint.dart';
+import 'package:collection/collection.dart';
+
+/// {@template avoid_build_context_extensions}
+/// The avoid_build_context_extensions lint rule.
+/// {@endtemplate}
+class AvoidBuildContextExtensions extends LintRule {
+  /// {@macro avoid_build_context_extensions}
+  AvoidBuildContextExtensions([Severity? severity])
+    : super(name: rule, severity: severity ?? Severity.warning);
+
+  /// The name of the lint rule.
+  static const rule = 'avoid_build_context_extensions';
+
+  @override
+  Listener create(LintContext context) => _Listener(context);
+}
+
+// Supported `BuildContext` extensions methods.
+enum _ContextMethod { read, watch, select }
+
+class _Listener extends Listener {
+  _Listener(this.context);
+
+  final LintContext context;
+
+  static const _declarationKeywords = {'final', 'const', 'var', 'late'};
+
+  /// Whether the type is implicitly a bloc type.
+  bool _isImplicitBlocType = false;
+
+  @override
+  void beginInitializedIdentifier(Token nameToken) {
+    final prev = nameToken.previous;
+    if (prev != null &&
+        prev.type == TokenType.IDENTIFIER &&
+        !_declarationKeywords.contains(prev.lexeme)) {
+      _isImplicitBlocType = prev.isBlocType;
+    }
+    super.beginInitializedIdentifier(nameToken);
+  }
+
+  @override
+  void endInitializedIdentifier(Token nameToken) {
+    _isImplicitBlocType = false;
+    super.endInitializedIdentifier(nameToken);
+  }
+
+  @override
+  void handleIdentifier(Token token, IdentifierContext _) {
+    final method = _ContextMethod.values.firstWhereOrNull(
+      (value) => value.name == token.lexeme,
+    );
+    if (method == null) return;
+
+    final prev = token.previous;
+    if (prev == null || prev.type != TokenType.PERIOD) return;
+
+    final target = prev.previous;
+    if (target == null) return;
+    if (target.lexeme != 'context') return;
+
+    // Case 1: implicit type
+    // e.g. final MyBloc bloc = context.read();
+    if (_isImplicitBlocType) return _report(method, target, token);
+
+    // Case 2: explicit type
+    // e.g. final bloc = context.read<MyBloc>();
+    final openBracketToken = token.next;
+    if (openBracketToken == null) return;
+    if (openBracketToken.type != TokenType.LT) return;
+
+    final typeToken = openBracketToken.next;
+    if (typeToken == null) return;
+    if (typeToken.type != TokenType.IDENTIFIER) return;
+    if (!typeToken.isBlocType) return;
+
+    return _report(method, target, token);
+  }
+
+  void _report(_ContextMethod method, Token beginToken, Token endToken) {
+    context.reportTokenRange(
+      beginToken: beginToken,
+      endToken: endToken,
+      message: 'Avoid using BuildContext extensions.',
+      hint: 'Prefer using ${method.alternative} instead.',
+    );
+  }
+}
+
+extension on _ContextMethod {
+  String get alternative {
+    switch (this) {
+      case _ContextMethod.read:
+        return 'BlocProvider.of<Bloc>(context, listen: false)';
+      case _ContextMethod.watch:
+        return '''BlocBuilder<Bloc, State>(...) or BlocProvider.of<Bloc>(context)''';
+      case _ContextMethod.select:
+        return 'BlocSelector<Bloc, State>(...)';
+    }
+  }
+}
+
+extension on Token {
+  bool get isBlocType {
+    return lexeme.endsWith('Bloc') || lexeme.endsWith('Cubit');
+  }
+}
diff --git a/packages/bloc_lint/lib/src/rules/rules.dart b/packages/bloc_lint/lib/src/rules/rules.dart
index 9c9e2865ada..b3979cc2bb0 100644
--- a/packages/bloc_lint/lib/src/rules/rules.dart
+++ b/packages/bloc_lint/lib/src/rules/rules.dart
@@ -1,3 +1,4 @@
+export 'avoid_build_context_extensions.dart';
 export 'avoid_flutter_imports.dart';
 export 'avoid_public_bloc_methods.dart';
 export 'avoid_public_fields.dart';


Test patch:
diff --git a/packages/bloc_lint/test/src/rules/avoid_build_context_extensions_test.dart b/packages/bloc_lint/test/src/rules/avoid_build_context_extensions_test.dart
new file mode 100644
index 00000000000..4c877fe86b5
--- /dev/null
+++ b/packages/bloc_lint/test/src/rules/avoid_build_context_extensions_test.dart
@@ -0,0 +1,487 @@
+import 'package:bloc_lint/src/rules/rules.dart';
+import 'package:test/test.dart';
+
+import '../lint_test_helper.dart';
+
+void main() {
+  group(AvoidBuildContextExtensions, () {
+    group('lints when', () {
+      group('context.read', () {
+        lintTest(
+          'is used in a field assignment',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatefulWidget {
+  const MyWidget({super.key});
+
+  @override
+  State<MyWidget> createState() => _MyWidgetState();
+}
+
+class _MyWidgetState extends State<MyWidget> {
+  late final a = context.read<CounterCubit>();
+                 ^^^^^^^^^^^^
+
+  @override
+  Widget build(BuildContext context) {
+    return const SizedBox();
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a variable assignment',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final cubit = context.read<CounterCubit>();
+                  ^^^^^^^^^^^^
+    return const SizedBox();
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a method call',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    context.read<CounterCubit>().increment();
+    ^^^^^^^^^^^^
+    return const SizedBox();
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is declared with another variable on the same line',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final String someVar, counterBloc = context.read<CounterBloc>();
+                                        ^^^^^^^^^^^^
+    return const SizedBox();
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used with an inferred bloc type',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final CounterBloc counterBloc = context.read();
+                                    ^^^^^^^^^^^^
+    return const SizedBox();
+  }
+}
+''',
+        );
+      });
+
+      group('context.watch', () {
+        lintTest(
+          'is used in a getter',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatefulWidget {
+  const MyWidget({super.key});
+
+  @override
+  State<MyWidget> createState() => _MyWidgetState();
+}
+
+class _MyWidgetState extends State<MyWidget> {
+  CounterCubit get cubit => context.watch<CounterCubit>();
+                            ^^^^^^^^^^^^^
+
+  @override
+  Widget build(BuildContext context) {
+    return const SizedBox();
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a variable assignment',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final count = context.watch<CounterCubit>().state;
+                  ^^^^^^^^^^^^^
+    return Text(count.toString());
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a widget',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: r'''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Text('${context.watch<CounterCubit>().state}');
+                   ^^^^^^^^^^^^^
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a ternary operator',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final state = true ? context.watch<CounterCubit>().state : null;
+                         ^^^^^^^^^^^^^
+    return Text(state.toString());
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used with Cubit type',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final userCubit = context.watch<UserCubit>();
+                      ^^^^^^^^^^^^^
+    return const SizedBox();
+  }
+}
+''',
+        );
+      });
+
+      group('context.select', () {
+        lintTest(
+          'is used in a variable assignment',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final isEven = context.select<CounterCubit, int>((cubit) => cubit.state.isEven);
+                   ^^^^^^^^^^^^^^
+    return Text(isEven ? 'true' : 'false');
+  }
+}
+''',
+        );
+
+        lintTest(
+          'is used in a widget',
+          rule: AvoidBuildContextExtensions.new,
+          path: 'my_widget.dart',
+          content: r'''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Text('${context.select<CounterCubit, int>((cubit) => cubit.state)}');
+                   ^^^^^^^^^^^^^^
+  }
+}
+''',
+        );
+      });
+    });
+
+    group('does not lint when', () {
+      lintTest(
+        'using BlocProvider.of',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final cubit = BlocProvider.of<CounterCubit>(context);
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using BlocProvider(create: ...)',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => CounterCubit(),
+      child: const SizedBox(),
+    );
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using other method calls on context',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final size = context.size;
+    final theme = context.theme;
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context in a string',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final message = 'context.read is allowed';
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context.read with non-bloc types',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final provider = context.read<MyProvider>();
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context.watch with non-bloc types',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final provider = context.watch<MyProvider>();
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context.select with non-bloc types',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final value = context.select<MyProvider, String>((provider) => provider.value);
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context extensions with implicit dynamic types',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    // These would be invalid Dart but should not trigger our rule
+    context.read();
+    context.watch();
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'using context.read with type inference for non-bloc types',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+class MyWidget extends StatelessWidget {
+  const MyWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final MyProvider provider = context.read();
+    return const SizedBox();
+  }
+}
+''',
+      );
+
+      lintTest(
+        'the call target is not context (e.g. cascade)',
+        rule: AvoidBuildContextExtensions.new,
+        path: 'my_widget.dart',
+        content: '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MyThing {
+  MyThing read<T>() => this;
+}
+
+class MyWidget extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    final thing = MyThing()..read<CounterCubit>();
+    return const SizedBox();
+  }
+}
+''',
+      );
+    });
+  });
+}
diff --git a/packages/bloc_lint/test/src/rules/avoid_public_fields_test.dart b/packages/bloc_lint/test/src/rules/avoid_public_fields_test.dart
index 8ad540b8b01..27c4abca51d 100644
--- a/packages/bloc_lint/test/src/rules/avoid_public_fields_test.dart
+++ b/packages/bloc_lint/test/src/rules/avoid_public_fields_test.dart
@@ -17,7 +17,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc() : super(0);
 
   var count = 0;
-  ^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -34,7 +34,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc() : super(0);
 
   var count = 0;
-  ^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -50,7 +50,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit() : super(0);
 
   var count = 0;
-  ^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -67,7 +67,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc() : super(0);
 
   int count = 0;
-  ^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -83,7 +83,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit() : super(0);
 
   int count = 0;
-  ^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -100,7 +100,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   final int count;
-  ^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -116,7 +116,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit(this.count) : super(0);
 
   final int count;
-  ^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -134,7 +134,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   final _Count count;
-  ^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -151,7 +151,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit(this.count) : super(0);
 
   final _Count count;
-  ^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -169,7 +169,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   late _Count count;
-  ^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -187,7 +187,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   late final _Count count;
-  ^^^^^^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -204,7 +204,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit(this.count) : super(0);
 
   late final _Count count;
-  ^^^^^^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -221,7 +221,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   late int count;
-  ^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -237,7 +237,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit(this.count) : super(0);
 
   late int count;
-  ^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -254,7 +254,7 @@ class CounterBloc extends Bloc<CounterEvent, int> {
   CounterBloc(this.count) : super(0);
 
   late final int count;
-  ^^^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );
@@ -270,7 +270,7 @@ class CounterCubit extends Cubit<int> {
   CounterCubit(this.count) : super(0);
 
   late final int count;
-  ^^^^^^^^^^^^^^^^^^^^
+  ^^^^^^^^^^^^^^^^^^^^^
 }
 ''',
     );