[code_builder] Emit ignore_for_file bits correctly (#2406)
diff --git a/pkgs/code_builder/CHANGELOG.md b/pkgs/code_builder/CHANGELOG.md
index 7b11a5c..8c73217 100644
--- a/pkgs/code_builder/CHANGELOG.md
+++ b/pkgs/code_builder/CHANGELOG.md
@@ -1,5 +1,8 @@
 ## 4.12.0-wip
 
+* Ensure scoped lint ignores
+  (such as `// ignore_for_file: no_leading_underscores_for_library_prefixes`)
+  are emitted at the top of the file before library annotations and directives.
 * Allow single quotes in strings passed to `literalString(raw:true)`. This
   argument no longer guarantees a raw string is used, but results will have the
   same behavior.
diff --git a/pkgs/code_builder/lib/src/emitter.dart b/pkgs/code_builder/lib/src/emitter.dart
index d095a91..e82a9fe 100644
--- a/pkgs/code_builder/lib/src/emitter.dart
+++ b/pkgs/code_builder/lib/src/emitter.dart
@@ -480,20 +480,6 @@
         ..writeln();
     }
 
-    if (spec.ignoreForFile.isNotEmpty) {
-      final ignores = spec.ignoreForFile.toList()..sort();
-      final lines = ['// ignore_for_file: ${ignores.first}'];
-      for (var ignore in ignores.skip(1)) {
-        if (lines.last.length + 2 + ignore.length > 80) {
-          lines.add('// ignore_for_file: $ignore');
-        } else {
-          lines[lines.length - 1] = '${lines.last}, $ignore';
-        }
-      }
-      lines.forEach(output.writeln);
-      output.writeln();
-    }
-
     // Process the body first in order to prime the allocators.
     final body = StringBuffer();
     for (final spec in spec.body) {
@@ -503,16 +489,17 @@
       }
     }
 
-    spec.docs.forEach(output.writeln);
+    final header = StringBuffer();
+    spec.docs.forEach(header.writeln);
     for (var a in spec.annotations) {
-      visitAnnotation(a, output);
+      visitAnnotation(a, header);
     }
     if (spec.name != null) {
-      output.write('library ${spec.name!};');
+      header.write('library ${spec.name!};');
     } else if (spec.annotations.isNotEmpty || spec.docs.isNotEmpty) {
       // An explicit _unnamed_ library directive is only required if there are
       // annotations or doc comments on the library.
-      output.write('library;');
+      header.write('library;');
     }
 
     final directives = <Directive>[...allocator.imports, ...spec.directives];
@@ -521,12 +508,32 @@
       directives.sort();
     }
 
-    Directive? previous;
-    if (directives.any((d) => d.as?.startsWith('_') ?? false)) {
-      output.writeln(
-        '// ignore_for_file: no_leading_underscores_for_library_prefixes',
-      );
+    final ignores = [
+      ...spec.ignoreForFile,
+      if (directives.any((d) => d.as?.startsWith('_') ?? false))
+        'no_leading_underscores_for_library_prefixes',
+    ];
+
+    if (ignores.isNotEmpty) {
+      ignores.sort();
+      const prefix = '// ignore_for_file: ';
+      final lines = [StringBuffer(prefix)..write(ignores.first)];
+      for (var ignore in ignores.skip(1)) {
+        if (lines.last.length + 2 + ignore.length > 80) {
+          lines.add(StringBuffer(prefix)..write(ignore));
+        } else {
+          lines.last
+            ..write(', ')
+            ..write(ignore);
+        }
+      }
+      lines.forEach(output.writeln);
+      output.writeln();
     }
+
+    output.write(header);
+
+    Directive? previous;
     for (final directive in directives) {
       if (_newLineBetween(orderDirectives, previous, directive)) {
         // Note: dartfmt handles creating new lines between directives.
diff --git a/pkgs/code_builder/test/directive_test.dart b/pkgs/code_builder/test/directive_test.dart
index 31bd2a4..33485cd 100644
--- a/pkgs/code_builder/test/directive_test.dart
+++ b/pkgs/code_builder/test/directive_test.dart
@@ -73,6 +73,7 @@
       library,
       equalsDart(r'''
           // ignore_for_file: no_leading_underscores_for_library_prefixes
+
           import '../relative.dart' as _i1;
           import 'package:foo/foo.dart' as _i2;
           import 'package:foo/bar.dart' as _i3;
@@ -94,6 +95,7 @@
       equalsDart(
         r'''
           // ignore_for_file: no_leading_underscores_for_library_prefixes
+
           import 'dart:collection' as _i4;
 
           import 'package:foo/bar.dart' as _i3;
diff --git a/pkgs/code_builder/test/specs/library_test.dart b/pkgs/code_builder/test/specs/library_test.dart
index 426f442..d8446c9 100644
--- a/pkgs/code_builder/test/specs/library_test.dart
+++ b/pkgs/code_builder/test/specs/library_test.dart
@@ -148,7 +148,7 @@
         ),
         equalsDart(r'''
             import 'dart:collection';
-          
+
             final test = LinkedHashMap();
           ''', DartEmitter()),
       );
@@ -224,7 +224,7 @@
         ),
         equalsDart(r'''
           import 'dart:collection';
-          
+
           final test = LinkedHashMap();
         ''', DartEmitter(allocator: Allocator())),
       );
@@ -249,8 +249,9 @@
         ),
         equalsDart(r'''
           // ignore_for_file: no_leading_underscores_for_library_prefixes
+
           import 'dart:collection' as _i1;
-          
+
           final test = _i1.LinkedHashMap();
         ''', DartEmitter(allocator: Allocator.simplePrefixing())),
       );
@@ -312,5 +313,41 @@
       '''),
       );
     });
+
+    test(
+      'should emit scoped lint ignores before library annotations, directives',
+      () {
+        expect(
+          Library(
+            (b) =>
+                b
+                  ..annotations.add(
+                    refer(
+                      'JS',
+                      'dart:js_interop',
+                    ).call([literalString('vscode')]),
+                  )
+                  ..body.add(
+                    Method(
+                      (m) =>
+                          m
+                            ..name = 'foo'
+                            ..external = true,
+                    ),
+                  ),
+          ),
+          equalsDart(r'''
+            // ignore_for_file: no_leading_underscores_for_library_prefixes
+
+            @_i1.JS('vscode')
+            library;
+
+            import 'dart:js_interop' as _i1;
+
+            external foo();
+          ''', DartEmitter.scoped()),
+        );
+      },
+    );
   });
 }