Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions source_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 63 additions & 29 deletions source_gen/lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
}
29 changes: 29 additions & 0 deletions source_gen/lib/src/span_for_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>()!;
Expand All @@ -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);
}
45 changes: 45 additions & 0 deletions source_gen/test/builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions source_gen/test/src/comment_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
}
}
Expand Down
Loading