Skip to content

Implement type parameters for annotations/metadata #2599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
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
494 changes: 253 additions & 241 deletions lib/src/generator/templates.renderers.dart

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions lib/src/model/annotation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:analyzer/dart/element/element.dart';
import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/model/getter_setter_combo.dart';
import 'package:dartdoc/src/model/library.dart';
import 'package:dartdoc/src/model/model_element.dart';
import 'package:dartdoc/src/model/nameable.dart';
import 'package:dartdoc/src/model/package_graph.dart';
import 'package:dartdoc/src/model/privacy.dart';

/// Represents a Dart annotation, attached to an element in the source code with
/// `@`.
class Annotation extends Privacy with Nameable {
final ElementAnnotation annotation;
final Library library;
final PackageGraph packageGraph;

Annotation(this.annotation, this.library, this.packageGraph);

String _renderedAnnotation;
String get renderedAnnotation => _renderedAnnotation ??=
'@' + linkedName + (const HtmlEscape()).convert(parameterText);

@override
String get name => annotation.element.name;

/// Return the linked name of the annotation.
String get linkedName => annotation.element is PropertyAccessorElement
? ModelElement.fromElement(annotation.element, packageGraph).linkedName
: modelType.linkedName;

ElementType _modelType;
ElementType get modelType {
if (_modelType == null) {
var annotatedWith = annotation.element;
if (annotatedWith is ConstructorElement) {
_modelType =
ElementType.from(annotatedWith.returnType, library, packageGraph);
} else if (annotatedWith is PropertyAccessorElement) {
_modelType =
(ModelElement.fromElement(annotatedWith.variable, packageGraph)
as GetterSetterCombo)
.modelType;
} else {
assert(false,
'non-callable element used as annotation?: ${annotation.element}');
}
}
return _modelType;
}

String _parameterText;
String get parameterText {
// TODO(srawlins): Attempt to revive constructor arguments in an annotation,
// akin to source_gen's Reviver, in order to link to inner components. For
// example, in `@Foo(const Bar(), baz: <Baz>[Baz.one, Baz.two])`, link to
// `Foo`, `Bar`, `Baz`, `Baz.one`, and `Baz.two`.
if (_parameterText == null) {
var source = annotation.toSource();
var startIndex = source.indexOf('(');
_parameterText =
source.substring(startIndex == -1 ? source.length : startIndex);
}
return _parameterText;
}

@override
bool get isPublic =>
modelType.isPublic &&
modelType is DefinedElementType &&
!packageGraph.invisibleAnnotations
.contains((modelType as DefinedElementType).element);
}
4 changes: 2 additions & 2 deletions lib/src/model/constructor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ class Constructor extends ModelElement
@override
String get kind => 'constructor';

DefinedElementType _modelType;
DefinedElementType get modelType =>
CallableElementTypeMixin _modelType;
CallableElementTypeMixin get modelType =>
_modelType ??= ElementType.from(element.type, library, packageGraph);

String _name;
Expand Down
8 changes: 2 additions & 6 deletions lib/src/model/field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,8 @@ class Field extends ModelElement
@override
List<String> get annotations {
var allAnnotations = [...super.annotations];

if (element is PropertyInducingElement) {
var pie = element as PropertyInducingElement;
allAnnotations.addAll(annotationsFromMetadata(pie.getter?.metadata));
allAnnotations.addAll(annotationsFromMetadata(pie.setter?.metadata));
}
if (hasGetter) allAnnotations.addAll(getter.annotations);
if (hasSetter) allAnnotations.addAll(setter.annotations);
return allAnnotations;
}

Expand Down
68 changes: 8 additions & 60 deletions lib/src/model/model_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:analyzer/src/dart/element/member.dart'
show ExecutableMember, Member;
import 'package:collection/collection.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/model/annotation.dart';
import 'package:dartdoc/src/model/documentation_comment.dart';
import 'package:dartdoc/src/model/feature_set.dart';
import 'package:dartdoc/src/model/model.dart';
Expand Down Expand Up @@ -401,65 +402,13 @@ abstract class ModelElement extends Canonicalization
ModelNode get modelNode =>
_modelNode ??= packageGraph.getModelNodeFor(element);

List<String> get annotations => annotationsFromMetadata(element.metadata);

/// Returns linked annotations from a given metadata set, with escaping.
// TODO(srawlins): Attempt to revive constructor arguments in an annotation,
// akin to source_gen's Reviver, in order to link to inner components. For
// example, in `@Foo(const Bar(), baz: <Baz>[Baz.one, Baz.two])`, link to
// `Foo`, `Bar`, `Baz`, `Baz.one`, and `Baz.two`.
List<String> annotationsFromMetadata(Iterable<ElementAnnotation> md) {
var annotationStrings = <String>[];
if (md == null) return annotationStrings;
for (var a in md) {
var annotation = (const HtmlEscape()).convert(a.toSource());
var annotationElement = a.element;

if (annotationElement is ConstructorElement) {
// TODO(srawlins): I think we should actually link to the constructor,
// which may have details about parameters. For example, given the
// annotation `@Immutable('text')`, the constructor documents what the
// parameter is, and the class only references `immutable`. It's a
// lose-lose cycle of mis-direction.
annotationElement =
(annotationElement as ConstructorElement).returnType.element;
} else if (annotationElement is PropertyAccessorElement) {
annotationElement =
(annotationElement as PropertyAccessorElement).variable;
}
if (annotationElement is Member) {
annotationElement = (annotationElement as Member).declaration;
}

// Some annotations are intended to be invisible (such as `@pragma`).
if (!_shouldDisplayAnnotation(annotationElement)) continue;
Iterable<String> get annotations =>
modelAnnotations.map((a) => a.renderedAnnotation);

var annotationModelElement =
packageGraph.findCanonicalModelElementFor(annotationElement);
if (annotationModelElement != null) {
annotation = annotation.replaceFirst(
annotationModelElement.name, annotationModelElement.linkedName);
}
annotationStrings.add(annotation);
}
return annotationStrings;
}

bool _shouldDisplayAnnotation(Element annotationElement) {
if (annotationElement is ClassElement) {
var annotationClass =
packageGraph.findCanonicalModelElementFor(annotationElement) as Class;
if (annotationClass == null && annotationElement != null) {
annotationClass =
ModelElement.fromElement(annotationElement, packageGraph) as Class;
}

return annotationClass == null ||
packageGraph.isAnnotationVisible(annotationClass);
}
// We cannot resolve it, which does not prevent it from being displayed.
return true;
}
Iterable<Annotation> _modelAnnotations;
// TODO(jcollins-g): rename to annotations and rework templates.
Iterable<Annotation> get modelAnnotations => _modelAnnotations ??=
element.metadata.map((m) => Annotation(m, library, packageGraph));

bool _isPublic;

Expand Down Expand Up @@ -531,8 +480,7 @@ abstract class ModelElement extends Canonicalization

Set<String> get features {
return {
...annotationsFromMetadata(element.metadata
.where((e) => !_specialFeatures.contains(e.element?.name))),
...annotations.where((a) => !_specialFeatures.contains(a)),
// 'const' and 'static' are not needed here because 'const' and 'static'
// elements get their own sections in the doc.
if (isFinal) 'final',
Expand Down
46 changes: 46 additions & 0 deletions test/end2end/model_special_cases_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,55 @@ void main() {

final _generalizedTypedefsAllowed =
VersionRange(min: Version.parse('2.13.0-0'), includeMin: true);
//final _genericMetadataAllowed =
// VersionRange(min: Version.parse('2.13.0-0'), includeMin: true);

// Experimental features not yet enabled by default. Move tests out of this
// block when the feature is enabled by default.
group('Experiments', () {
group('generic metadata', () {
Library genericMetadata;
TopLevelVariable f;
Class C;
Method mp, mn;

setUpAll(() async {
genericMetadata = (await _testPackageGraphExperiments)
.libraries
.firstWhere((l) => l.name == 'generic_metadata');
f = genericMetadata.properties.firstWhere((p) => p.name == 'f');
C = genericMetadata.classes.firstWhere((c) => c.name == 'C');
mp = C.instanceMethods.firstWhere((m) => m.name == 'mp');
mn = C.instanceMethods.firstWhere((m) => m.name == 'mn');
});

test('Verify type arguments on annotations renders, including parameters',
() {
var ab0 =
'@<a href="%%__HTMLBASE_dartdoc_internal__%%generic_metadata/A-class.html">A</a><span class="signature">&lt;<wbr><span class="type-parameter"><a href="%%__HTMLBASE_dartdoc_internal__%%generic_metadata/B.html">B</a></span>&gt;</span>(0)';

expect(genericMetadata.annotations.first, equals(ab0));
expect(f.annotations.first, equals(ab0));
expect(C.annotations.first, equals(ab0));
expect(C.typeParameters.first.annotations.first, equals(ab0));
expect(mp.parameters.map((p) => p.annotations.first),
everyElement(equals(ab0)));
expect(mn.parameters.map((p) => p.annotations.first),
everyElement(equals(ab0)));

expect(genericMetadata.features, contains(ab0));
expect(f.features, contains(ab0));
expect(C.features, contains(ab0));
expect(C.typeParameters.first.features, contains(ab0));
expect(
mp.parameters.map((p) => p.features), everyElement(contains(ab0)));
expect(
mn.parameters.map((p) => p.features), everyElement(contains(ab0)));
});
}, skip: 'requires analyzer > 1.2.0'
//(!_genericMetadataAllowed.allows(_platformVersion))
);

group('generalized typedefs', () {
Library generalizedTypedefs;
Typedef T0, T1, T2, T3, T4, T5, T6, T7;
Expand Down
4 changes: 2 additions & 2 deletions test/end2end/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4041,7 +4041,7 @@ String topLevelFunction(int param1, bool param2, Cool coolBeans,

test('constructor annotations have the right link and are escaped', () {
expect(
ctr.annotations[0],
ctr.annotations.first,
equals(
'@<a href="${htmlBasePlaceholder}ex/Deprecated-class.html">Deprecated</a>'
'(&quot;Internal use&quot;)'));
Expand All @@ -4051,7 +4051,7 @@ String topLevelFunction(int param1, bool param2, Cool coolBeans,
var createDog2 =
dog.staticMethods.firstWhere((c) => c.name == 'createDog2');
expect(
createDog2.annotations[0],
createDog2.annotations.first,
equals(
'@<a href="${htmlBasePlaceholder}ex/deprecated-constant.html">deprecated</a>'));
});
Expand Down
1 change: 1 addition & 0 deletions testing/test_package_experiments/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ analyzer:
enable-experiment:
- non-nullable
- nonfunction-type-aliases
- generic-metadata
Loading