Repository: felangel/bloc (dart)

Issue:
feat(bloc_lint): support for // ignore
**Description**
bloc_lint does not take `// ignore` into account

**Expected Behavior**
Don't trigger linter

**Screenshots**
<img width="870" alt="Image" src="https://github.com/user-attachments/assets/084bcbc1-1c41-4fde-a22e-b272ca221919" />

![Image](https://github.com/user-attachments/assets/ff991767-9372-4e4c-b590-fbb26c930452)

Pull request description:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Status

**READY**

## Breaking Changes

NO

## Description

<!--- Describe your changes in detail -->
- feat(bloc_lint): support `// ignore` (closes #4467)

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [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/packages/bloc_lint/lib/src/linter.dart b/packages/bloc_lint/lib/src/linter.dart
index 40b0bff43eb..659b7bbcf1e 100644
--- a/packages/bloc_lint/lib/src/linter.dart
+++ b/packages/bloc_lint/lib/src/linter.dart
@@ -158,6 +158,7 @@ class LintContext {
     required String message,
     String hint = '',
   }) {
+    if (document.hasLineIgnore(range: range, rule: _rule.name)) return;
     _diagnostics.add(
       Diagnostic(
         range: range,
diff --git a/packages/bloc_lint/lib/src/text_document.dart b/packages/bloc_lint/lib/src/text_document.dart
index b7c92a32aa4..bcfc1055557 100644
--- a/packages/bloc_lint/lib/src/text_document.dart
+++ b/packages/bloc_lint/lib/src/text_document.dart
@@ -24,6 +24,8 @@ class TextDocument {
     multiLine: true,
   );
 
+  static final _ignoreForLineRegExp = RegExp(r'^//\s*ignore:(.*)$');
+
   final Uri _uri;
   final String _content;
   List<int>? _lineOffsets;
@@ -33,6 +35,51 @@ class TextDocument {
   /// have other schemes indicating that they are not available on disk.
   Uri get uri => _uri;
 
+  /// Whether the line for the current range contains an // ignore: for the given rule.
+  bool hasLineIgnore({required Range range, required String rule}) {
+    return _hasIgnoreAboveLine(range: range, rule: rule) ||
+        _hasIgnoreAfterLine(range: range, rule: rule);
+  }
+
+  bool _hasIgnoreAboveLine({required Range range, required String rule}) {
+    final previousLine = range.start.line - 1;
+    if (previousLine < 0) return false;
+    final line = getText(
+      range: Range(
+        start: Position(character: 0, line: previousLine),
+        end: Position(character: _content.length, line: previousLine),
+      ),
+    );
+    final lineIgnores = _lineIgnores(line);
+    return lineIgnores.contains(rule);
+  }
+
+  bool _hasIgnoreAfterLine({required Range range, required String rule}) {
+    final afterText = getText(
+      range: Range(
+        start: Position(character: range.end.character, line: range.end.line),
+        end: Position(character: _content.length, line: range.end.line),
+      ),
+    );
+    final index = afterText.indexOf('// ignore:');
+    if (index == -1) return false;
+    final line = afterText.substring(index);
+    final lineIgnores = _lineIgnores(line);
+    return lineIgnores.contains(rule);
+  }
+
+  Set<String> _lineIgnores(String line) {
+    final result = <String>{};
+    final matches = _ignoreForLineRegExp.allMatches(line);
+    if (matches.isEmpty) return result;
+    for (final match in matches) {
+      final contents = match.group(1);
+      if (contents == null) continue;
+      result.addAll(contents.split(',').map((segment) => segment.trim()));
+    }
+    return result;
+  }
+
   /// Returns a list of rules ignored for the current file.
   Set<String> get ignoreForFile {
     final result = <String>{};


Test patch:
diff --git a/packages/bloc_lint/test/src/linter_test.dart b/packages/bloc_lint/test/src/linter_test.dart
index 563eabb1881..9e196e6d74e 100644
--- a/packages/bloc_lint/test/src/linter_test.dart
+++ b/packages/bloc_lint/test/src/linter_test.dart
@@ -134,5 +134,37 @@ class CounterBloc extends Bloc<CounterEvent, int> {
 ''',
       );
     });
+
+    lintTest(
+      'does not report when rule is ignored for line (above)',
+      rule: AvoidFlutterImports.new,
+      path: 'counter_bloc.dart',
+      content: '''
+import 'package:bloc/bloc.dart';
+
+// ignore: ${AvoidFlutterImports.rule}
+import 'package:flutter/material.dart';
+
+enum CounterEvent { increment, decrement }
+class CounterBloc extends Bloc<CounterEvent, int> {
+  CounterBloc() : super(0);
+}
+''',
+    );
+
+    lintTest(
+      'does not report when rule is ignored for line (after)',
+      rule: AvoidFlutterImports.new,
+      path: 'counter_bloc.dart',
+      content: '''
+import 'package:bloc/bloc.dart';
+import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule}
+
+enum CounterEvent { increment, decrement }
+class CounterBloc extends Bloc<CounterEvent, int> {
+  CounterBloc() : super(0);
+}
+''',
+    );
   });
 }
diff --git a/packages/bloc_lint/test/src/text_document_test.dart b/packages/bloc_lint/test/src/text_document_test.dart
index 6e0fa03678d..18a9b282056 100644
--- a/packages/bloc_lint/test/src/text_document_test.dart
+++ b/packages/bloc_lint/test/src/text_document_test.dart
@@ -166,27 +166,33 @@ void main() {
       final document = TextDocument(
         uri: Uri.parse('counter_bloc.dart'),
         content: '''
-// ignore_for_file: prefer_bloc, prefer_cubit
+// ignore_for_file: ${PreferBlocLint.rule}, ${PreferCubitLint.rule}
 void main() {
   print("hello world");
 }
 ''',
       );
-      expect(document.ignoreForFile, equals({'prefer_bloc', 'prefer_cubit'}));
+      expect(
+        document.ignoreForFile,
+        equals({PreferBlocLint.rule, PreferCubitLint.rule}),
+      );
     });
 
     test('detects multiple ignore_for_file', () {
       final document = TextDocument(
         uri: Uri.parse('counter_bloc.dart'),
         content: '''
-// ignore_for_file: prefer_bloc
-// ignore_for_file: prefer_cubit
+// ignore_for_file: ${PreferBlocLint.rule}
+// ignore_for_file: ${PreferCubitLint.rule}
 void main() {
   print("hello world");
 }
 ''',
       );
-      expect(document.ignoreForFile, equals({'prefer_bloc', 'prefer_cubit'}));
+      expect(
+        document.ignoreForFile,
+        equals({PreferBlocLint.rule, PreferCubitLint.rule}),
+      );
     });
 
     test('ignore_for_file throughout file', () {
@@ -194,13 +200,150 @@ void main() {
         uri: Uri.parse('counter_bloc.dart'),
         content: '''
 void main() {
-// ignore_for_file: prefer_bloc
+// ignore_for_file: ${PreferBlocLint.rule}
   print("hello world");
 }
-// ignore_for_file: prefer_cubit
+// ignore_for_file: ${PreferCubitLint.rule}
 ''',
       );
-      expect(document.ignoreForFile, equals({'prefer_bloc', 'prefer_cubit'}));
+      expect(
+        document.ignoreForFile,
+        equals({PreferBlocLint.rule, PreferCubitLint.rule}),
+      );
+    });
+  });
+
+  group('hasLineIgnore', () {
+    test('returns false when no ignore exists', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+import 'package:flutter/material.dart';
+''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 0),
+        end: Position(character: 39, line: 0),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isFalse,
+      );
+    });
+
+    test('returns false when ignore not found (above)', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+// ignore: ${PreferBlocLint.rule}, ${PreferCubitLint.rule}
+import 'package:flutter/material.dart';''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 1),
+        end: Position(character: 39, line: 1),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isFalse,
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: PreferBlocLint.rule),
+        isTrue,
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: PreferCubitLint.rule),
+        isTrue,
+      );
+    });
+
+    test('returns false when ignore not found (after)', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+import 'package:flutter/material.dart'; // ignore: ${PreferBlocLint.rule}, ${PreferCubitLint.rule}''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 0),
+        end: Position(character: 39, line: 0),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isFalse,
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: PreferBlocLint.rule),
+        isTrue,
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: PreferCubitLint.rule),
+        isTrue,
+      );
+    });
+
+    test('returns true when ignore exists above', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+// ignore: ${AvoidFlutterImports.rule}
+import 'package:flutter/material.dart';''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 1),
+        end: Position(character: 39, line: 1),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isTrue,
+      );
+    });
+
+    test('returns true when multiple ignores exists above', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+// ignore: ${PreferBlocLint.rule}, ${AvoidFlutterImports.rule}
+import 'package:flutter/material.dart';''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 1),
+        end: Position(character: 39, line: 1),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isTrue,
+      );
+    });
+
+    test('returns true when ignore exists after', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule}''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 0),
+        end: Position(character: 39, line: 0),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isTrue,
+      );
+    });
+
+    test('returns true when multiple ignores exists after', () {
+      final document = TextDocument(
+        uri: Uri.parse('counter_cubit.dart'),
+        content: '''
+import 'package:flutter/material.dart'; // ignore: ${PreferBlocLint.rule}, ${AvoidFlutterImports.rule}''',
+      );
+      const range = Range(
+        start: Position(character: 0, line: 0),
+        end: Position(character: 39, line: 0),
+      );
+      expect(
+        document.hasLineIgnore(range: range, rule: AvoidFlutterImports.rule),
+        isTrue,
+      );
     });
   });
 }