Repository: dart-lang/source_gen

Issue:
InvalidSourceGenerationError should accept Fragments/Annotatable too.
With latest analyzer, the concept of `Element` has been split in two. We now have "Fragments". The problem is, `InvalidSourceGenerationError` does not accept this new kind of object. 

`Annotatable` has the same issue, since it's a base class for Fragment|Element|ElementDirective

If `InvalidSourceGenerationError` could accept those objects, then the error message would be more specific.


Discussion: Sounds good to me, thanks.

About to go on vacation for a week, but added it to the TODO list :)

Fix patch:
diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md
index bc7bf548..dbe4625a 100644
--- a/source_gen/CHANGELOG.md
+++ b/source_gen/CHANGELOG.md
@@ -6,6 +6,8 @@
 - Prepare to stop using `dart:mirrors`: add `inPackage` and `inSdk` parameters
   to `GenerateForAnnotation`. These will start being used in version `4.0.0`
   when it switches to `TypeChecker.typeNamed`.
+- `InvalidGenerationSource` support for `Fragment`, `ElementDirective` and
+  `Annotatable`.
 - Allow `analyzer: '>=7.4.0 <9.0.0'`.
 
 ## 3.0.0
diff --git a/source_gen/lib/src/generator.dart b/source_gen/lib/src/generator.dart
index 72483a92..3d8eba36 100644
--- a/source_gen/lib/src/generator.dart
+++ b/source_gen/lib/src/generator.dart
@@ -7,6 +7,7 @@ import 'dart:async';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/element/element2.dart';
 import 'package:build/build.dart';
+import 'package:source_span/source_span.dart';
 
 import 'library.dart';
 import 'span_for_element.dart';
@@ -45,59 +46,92 @@ class InvalidGenerationSource implements Exception {
   /// May be an empty string if unknown.
   final String todo;
 
-  /// The code element associated with this error.
-  ///
-  /// May be `null` if the error had no associated element, or if the location
-  /// was passed with [node].
+  /// The [Element2] associated with this error, if any.
   final Element2? element;
 
-  /// The AST Node associated with this error.
-  ///
-  /// May be `null` if the error has no associated node in the input source
-  /// code, or if the location was passed with [element].
+  /// The [ElementDirective] associated with this error, if any.
+  final ElementDirective? elementDirective;
+
+  /// The [AstNode] associated with this error, if any.
   final AstNode? node;
 
+  /// The [Fragment] associated with this error, if any.
+  final Fragment? fragment;
+
   InvalidGenerationSource(
     this.message, {
+    Annotatable? annotatable,
     this.todo = '',
-    this.element,
+    Element2? element,
+    ElementDirective? elementDirective,
+    Fragment? fragment,
     this.node,
-  });
+  }) : element =
+           element ??
+           (annotatable is Element2 ? annotatable : null) as Element2?,
+       elementDirective =
+           elementDirective ??
+           (annotatable is ElementDirective ? annotatable : null),
+       fragment =
+           fragment ??
+           (annotatable is Fragment ? annotatable : null) as Fragment?;
 
   @override
   String toString() {
     final buffer = StringBuffer(message);
 
+    // If possible render a span, if a span can't be computed show any cause
+    // object.
+    SourceSpan? span;
+    Object? cause;
+
     if (element case final element?) {
       try {
-        final span = spanForElement(element);
-        buffer
-          ..writeln()
-          ..writeln(span.start.toolString)
-          ..write(span.highlight());
+        span = spanForElement(element);
       } catch (_) {
-        // Source for `element` wasn't found, it must be in a summary with no
-        // associated source. We can still give the name.
-        buffer
-          ..writeln()
-          ..writeln('Cause: $element');
+        cause = element;
       }
     }
 
-    if (node case final node?) {
+    if (elementDirective case final elementDirective?) {
       try {
-        final span = spanForNode(node);
-        buffer
-          ..writeln()
-          ..writeln(span.start.toolString)
-          ..write(span.highlight());
+        span = spanForElementDirective(elementDirective);
       } catch (_) {
-        buffer
-          ..writeln()
-          ..writeln('Cause: $node');
+        cause = elementDirective;
       }
     }
 
+    if (span == null) {
+      if (node case final node?) {
+        try {
+          span = spanForNode(node);
+        } catch (_) {
+          cause = node;
+        }
+      }
+    }
+
+    if (span == null) {
+      if (fragment case final fragment?) {
+        try {
+          span = spanForFragment(fragment);
+        } catch (_) {
+          cause = fragment;
+        }
+      }
+    }
+
+    if (span != null) {
+      buffer
+        ..writeln()
+        ..writeln(span.start.toolString)
+        ..write(span.highlight());
+    } else if (cause != null) {
+      buffer
+        ..writeln()
+        ..writeln('Cause: $cause');
+    }
+
     return buffer.toString();
   }
 }
diff --git a/source_gen/lib/src/span_for_element.dart b/source_gen/lib/src/span_for_element.dart
index a0dd38ef..edbb6420 100644
--- a/source_gen/lib/src/span_for_element.dart
+++ b/source_gen/lib/src/span_for_element.dart
@@ -54,6 +54,24 @@ SourceSpan spanForElement(Element2 element, [SourceFile? file]) {
   );
 }
 
+/// Returns a source span for the start character of [elementDirective].
+SourceSpan spanForElementDirective(ElementDirective elementDirective) {
+  final libraryFragment = elementDirective.libraryFragment;
+  final contents = libraryFragment.source.contents.data;
+  final url = assetToPackageUrl(libraryFragment.source.uri);
+  final file = SourceFile.fromString(contents, url: url);
+  var offset = 0;
+  if (elementDirective is LibraryExport) {
+    offset = elementDirective.exportKeywordOffset;
+  } else if (elementDirective is LibraryImport) {
+    offset = elementDirective.importKeywordOffset;
+  } else if (elementDirective is PartInclude) {
+    // TODO(davidmorgan): no way to get this yet, see
+    // https://github.com/dart-lang/source_gen/issues/769#issuecomment-3157032889
+  }
+  return file.span(offset, offset);
+}
+
 /// Returns a source span that spans the location where [node] is written.
 SourceSpan spanForNode(AstNode node) {
   final unit = node.thisOrAncestorOfType<CompilationUnit>()!;
@@ -63,3 +81,14 @@ SourceSpan spanForNode(AstNode node) {
   final file = SourceFile.fromString(contents, url: url);
   return file.span(node.offset, node.offset + node.length);
 }
+
+/// Returns a source span for the start character of [fragment].
+///
+/// If the fragment has a name, the start character is the start of the name.
+SourceSpan spanForFragment(Fragment fragment) {
+  final libraryFragment = fragment.libraryFragment!;
+  final contents = libraryFragment.source.contents.data;
+  final url = assetToPackageUrl(libraryFragment.source.uri);
+  final file = SourceFile.fromString(contents, url: url);
+  return file.span(fragment.offset, fragment.offset);
+}


Test patch:
diff --git a/source_gen/test/builder_test.dart b/source_gen/test/builder_test.dart
index 1a42d4da..f097d01b 100644
--- a/source_gen/test/builder_test.dart
+++ b/source_gen/test/builder_test.dart
@@ -207,6 +207,38 @@ $dartFormatWidth
       logs,
       contains(contains("Don't use classes with the word 'Error' in the name")),
     );
+    // The class name starts at line 4, column 7.
+    expect(logs, contains(contains(':4:7\n')));
+  });
+
+  test('handle generator errors reported using fragments', () async {
+    final srcs = _createPackageStub(
+      testLibContent: _testLibContentWithErrorFragment,
+    );
+    final builder = PartBuilder([const CommentGenerator()], '.foo.dart');
+    final logs = <String>[];
+    await testBuilder(builder, srcs, onLog: (r) => logs.add(r.toString()));
+    expect(
+      logs,
+      contains(contains("Don't use classes with the word 'Error' in the name")),
+    );
+    // The class name starts at line 3, column 7.
+    expect(logs, contains(contains(':3:7\n')));
+  });
+
+  test('handle generator errors reported using element directives', () async {
+    final srcs = _createPackageStub(
+      testLibContent: _testLibContentWithErrorElementDirective,
+    );
+    final builder = PartBuilder([const CommentGenerator()], '.foo.dart');
+    final logs = <String>[];
+    await testBuilder(builder, srcs, onLog: (r) => logs.add(r.toString()));
+    expect(
+      logs,
+      contains(contains("Don't use classes with the word 'Error' in the name")),
+    );
+    // The export directive starts at line 2, column 1.
+    expect(logs, contains(contains(':2:1\n')));
   });
 
   test('throws when input library has syntax errors and allowSyntaxErrors '
@@ -996,6 +1028,19 @@ class MyError { }
 class MyGoodError { }
 ''';
 
+const _testLibContentWithErrorFragment = r'''
+library test_lib;
+part 'test_lib.foo.dart';
+class MyFragmentError { }
+''';
+
+const _testLibContentWithErrorElementDirective = r'''
+library test_lib;
+export 'foo.dart';
+part 'test_lib.foo.dart';
+class MyElementDirectiveError { }
+''';
+
 const _testLibPartContent = r'''
 part of 'test_lib.dart';
 final int bar = 42;
diff --git a/source_gen/test/src/comment_generator.dart b/source_gen/test/src/comment_generator.dart
index b85605b5..115a9a29 100644
--- a/source_gen/test/src/comment_generator.dart
+++ b/source_gen/test/src/comment_generator.dart
@@ -30,6 +30,23 @@ class CommentGenerator extends Generator {
             element: classElement,
           );
         }
+        if (classElement.displayName.contains('FragmentError')) {
+          throw InvalidGenerationSourceError(
+            "Don't use classes with the word 'Error' in the name",
+            todo: 'Rename ${classElement.displayName} to something else.',
+            fragment: classElement.firstFragment,
+          );
+        }
+        if (classElement.displayName.contains('ElementDirectiveError')) {
+          throw InvalidGenerationSourceError(
+            "Don't use classes with the word 'Error' in the name",
+            todo: 'Rename ${classElement.displayName} to something else.',
+            // No directive relates to the class, just throw with the first
+            // export.
+            elementDirective:
+                classElement.library2.firstFragment.libraryExports2.first,
+          );
+        }
         output.add('// Code for "$classElement"');
       }
     }