From a93632fc3887b2865a10ddb1e6ef61c38da2eae6 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 5 Aug 2025 11:31:21 +0200 Subject: [PATCH] Support fragments, element directives and annotatables in InvalidGenerationSource. --- source_gen/CHANGELOG.md | 4 +- source_gen/lib/src/generator.dart | 92 +++++++++++++++------- source_gen/lib/src/span_for_element.dart | 29 +++++++ source_gen/pubspec.yaml | 2 +- source_gen/test/builder_test.dart | 45 +++++++++++ source_gen/test/src/comment_generator.dart | 17 ++++ 6 files changed, 158 insertions(+), 31 deletions(-) diff --git a/source_gen/CHANGELOG.md b/source_gen/CHANGELOG.md index 68715a03..7278b9ee 100644 --- a/source_gen/CHANGELOG.md +++ b/source_gen/CHANGELOG.md @@ -1,5 +1,7 @@ -## 3.0.1-wip +## 3.1.0-wip +- `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()!; @@ -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); +} diff --git a/source_gen/pubspec.yaml b/source_gen/pubspec.yaml index 2949555a..aa6da555 100644 --- a/source_gen/pubspec.yaml +++ b/source_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: source_gen -version: 3.0.1-wip +version: 3.1.0-wip description: >- Source code generation builders and utilities for the Dart build system repository: https://github.com/dart-lang/source_gen/tree/master/source_gen 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 = []; + 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 = []; + 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"'); } }