From 34af38030b08aefba6ef44f857e427b6356188f6 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 3 Oct 2025 15:06:27 -0700 Subject: [PATCH 1/3] Display constructor names in annotations --- lib/src/model/annotation.dart | 43 ++++++++++++++++++++++--- lib/src/model/constructor.dart | 8 +++-- test/annotations_test.dart | 42 ++++++++++++++++++------ test/end2end/model_test.dart | 4 ++- test/enums_test.dart | 2 +- test/templates/class_test.dart | 2 +- test/templates/enum_test.dart | 2 +- test/templates/extension_type_test.dart | 2 +- test/templates/field_test.dart | 2 +- test/templates/method_test.dart | 3 +- 10 files changed, 85 insertions(+), 25 deletions(-) diff --git a/lib/src/model/annotation.dart b/lib/src/model/annotation.dart index 63576d3ce2..a0982bd85a 100644 --- a/lib/src/model/annotation.dart +++ b/lib/src/model/annotation.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/src/dart/element/member.dart'; import 'package:dartdoc/src/element_type.dart'; import 'package:dartdoc/src/model/attribute.dart'; import 'package:dartdoc/src/model/class.dart'; @@ -36,14 +37,46 @@ final class Annotation extends Attribute { var parameterText = source.substring(startIndex == -1 ? source.length : startIndex); - return '@$linkedName${const HtmlEscape().convert(parameterText)}'; + var escapedParameterText = const HtmlEscape().convert(parameterText); + return '@$linkedName$_linkedTypeArguments$escapedParameterText'; } @override - String get linkedName => _annotation.element is PropertyAccessorElement - ? _packageGraph.getModelForElement(_annotation.element!).linkedName - // TODO(jcollins-g): consider linking to constructor instead of type? - : _modelType.linkedName; + String get linkedName => switch (_annotation.element) { + PropertyAccessorElement element => + _packageGraph.getModelForElement(element).linkedName, + ConstructorElement element => + _packageGraph.getModelForElement(element).linkedName, + _ => _modelType.linkedName + }; + + /// The linked type argument text, with `<` and `>`, if there are any type + /// arguments. + String get _linkedTypeArguments { + var element = _annotation.element; + if (element is! SubstitutedConstructorElementImpl) { + return ''; + } + + var buffer = StringBuffer(); + buffer.write('<'); + var container = element.baseElement.enclosingElement; + for (var p in container.typeParameters) { + var type = element.substitution.map[p]; + assert(type != null); + if (type == null) { + // Abandon this type arguments string, as something is wrong with the + // user's code. + return ''; + } + buffer.write(_packageGraph.getTypeFor(type, _library).linkedName); + if (p != container.typeParameters.last) { + buffer.write(', '); + } + } + buffer.write('>'); + return buffer.toString(); + } late final ElementType _modelType = switch (_annotation.element) { ConstructorElement(:var returnType) => diff --git a/lib/src/model/constructor.dart b/lib/src/model/constructor.dart index 0503b76978..8152901345 100644 --- a/lib/src/model/constructor.dart +++ b/lib/src/model/constructor.dart @@ -92,8 +92,7 @@ class Constructor extends ModelElement with ContainerMember, TypeParameters { @override Kind get kind => Kind.constructor; - late final Callable modelType = - getTypeFor(element.type, library) as Callable; + late final Callable modelType = getTypeFor(element.type, library) as Callable; @override String get name => @@ -102,6 +101,11 @@ class Constructor extends ModelElement with ContainerMember, TypeParameters { // code there and elsewhere with simple references to the name. '${enclosingElement.name}.${element.name}'; + @override + String get displayName => isUnnamedConstructor + ? enclosingElement.name + : '${enclosingElement.name}.${element.name}'; + @override String get nameWithGenerics { var constructorName = element.name!; diff --git a/test/annotations_test.dart b/test/annotations_test.dart index 69f63dfc9b..c972de574d 100644 --- a/test/annotations_test.dart +++ b/test/annotations_test.dart @@ -48,11 +48,11 @@ int value = 0; var annotation = valueVariable.annotations.single; expect( annotation.linkedName, - 'Deprecated', + 'Deprecated', ); expect( annotation.linkedNameWithParameters, - '@Deprecated' + '@Deprecated' '('text')', ); } @@ -97,16 +97,40 @@ int value = 0; var annotation = valueVariable.annotations.single; expect( annotation.linkedName, - '' + '' 'MyAnnotation', ); expect( annotation.linkedNameWithParameters, - '@' + '@' 'MyAnnotation(true)', ); } + void test_locallyDeclaredConstructorCall_named() async { + var library = await bootPackageWithLibrary(''' +class MyAnnotation { + const MyAnnotation.named(bool b); +} + +@MyAnnotation.named(true) +int value = 0; +'''); + var valueVariable = library.properties.named('value'); + expect(valueVariable.hasAnnotations, true); + var annotation = valueVariable.annotations.single; + expect( + annotation.linkedName, + '' + 'MyAnnotation.named', + ); + expect( + annotation.linkedNameWithParameters, + '@' + 'MyAnnotation.named(true)', + ); + } + void test_genericConstructorCall() async { var library = await bootPackageWithLibrary(''' class Ann { @@ -122,15 +146,13 @@ int value = 0; var annotation = valueVariable.annotations.single; expect( annotation.linkedName, - 'Ann' - '<' - 'bool>', + 'Ann', ); expect( annotation.linkedNameWithParameters, - '@Ann' - '<' - 'bool>(true)', + '@Ann' + '<bool>' + '(true)', ); } } diff --git a/test/end2end/model_test.dart b/test/end2end/model_test.dart index 4f6fe4034f..bcda54076c 100644 --- a/test/end2end/model_test.dart +++ b/test/end2end/model_test.dart @@ -190,7 +190,9 @@ void main() async { test('Verify type arguments on annotations renders, including parameters', () { var ab0 = - '@A<B>(0)'; + '@A' + '<B>' + '(0)'; expect(genericMetadata.annotations.first.linkedNameWithParameters, equals(ab0)); diff --git a/test/enums_test.dart b/test/enums_test.dart index 224cbe02c8..80fb8514c2 100644 --- a/test/enums_test.dart +++ b/test/enums_test.dart @@ -207,7 +207,7 @@ enum E { one, two, three } expect(eEnum.hasAnnotations, true); expect(eEnum.annotations, hasLength(1)); expect(eEnum.annotations.single.linkedName, - 'C'); + 'C'); } void test_hasDocComment() async { diff --git a/test/templates/class_test.dart b/test/templates/class_test.dart index c70ef2bde0..2e46a46a88 100644 --- a/test/templates/class_test.dart +++ b/test/templates/class_test.dart @@ -218,7 +218,7 @@ class C { htmlLines.expectMainContentContainsAllInOrder([ matches('

Constructors

'), - matches('C.new'), + matches('C'), matches('An unnamed constructor.'), ]); } diff --git a/test/templates/enum_test.dart b/test/templates/enum_test.dart index 594714b50c..23a75439b9 100644 --- a/test/templates/enum_test.dart +++ b/test/templates/enum_test.dart @@ -180,7 +180,7 @@ extension Ext on E {} matches('
Annotations
'), matches('
    '), matches( - r'
  • @C\('message'\)
  • '), + r'
  • @C<dynamic>\('message'\)
  • '), matches('
'), ]); }); diff --git a/test/templates/extension_type_test.dart b/test/templates/extension_type_test.dart index 4d50a24844..c15eee8a0e 100644 --- a/test/templates/extension_type_test.dart +++ b/test/templates/extension_type_test.dart @@ -166,7 +166,7 @@ extension type One(int it) { htmlLines.expectMainContentContainsAllInOrder([ matches('

Constructors

'), - matches('One.new'), + matches('One'), matches('One.named'), matches('A named constructor.'), ]); diff --git a/test/templates/field_test.dart b/test/templates/field_test.dart index e23f791054..32e8a76014 100644 --- a/test/templates/field_test.dart +++ b/test/templates/field_test.dart @@ -54,7 +54,7 @@ class A { matches('
    '), matches('
  1. @deprecated
  2. '), matches( - r'
  3. @A\('message'\)
  4. '), + r'
  5. @A\('message'\)
  6. '), matches('
'), ], ); diff --git a/test/templates/method_test.dart b/test/templates/method_test.dart index a87be07beb..41d887789b 100644 --- a/test/templates/method_test.dart +++ b/test/templates/method_test.dart @@ -6,7 +6,6 @@ import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../src/utils.dart'; - import 'template_test_base.dart'; void main() async { @@ -53,7 +52,7 @@ class C { matches('
    '), matches('
  1. @deprecated
  2. '), matches( - r'
  3. @A\('message'\)
  4. '), + r'
  5. @A\('message'\)
  6. '), matches('
'), ], ); From 6a70a273185524e254e37c27cff460d8c2676058 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 3 Oct 2025 15:43:43 -0700 Subject: [PATCH 2/3] comment --- lib/src/model/annotation.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/model/annotation.dart b/lib/src/model/annotation.dart index a0982bd85a..5013e58285 100644 --- a/lib/src/model/annotation.dart +++ b/lib/src/model/annotation.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:analyzer/dart/element/element.dart'; +// ignore: implementation_imports import 'package:analyzer/src/dart/element/member.dart'; import 'package:dartdoc/src/element_type.dart'; import 'package:dartdoc/src/model/attribute.dart'; From 9511f673cc49914d4089392811e37084dd6cd002 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Sat, 4 Oct 2025 12:12:55 -0700 Subject: [PATCH 3/3] avoid impl --- lib/src/model/annotation.dart | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/src/model/annotation.dart b/lib/src/model/annotation.dart index 5013e58285..f29178c31c 100644 --- a/lib/src/model/annotation.dart +++ b/lib/src/model/annotation.dart @@ -5,8 +5,7 @@ import 'dart:convert'; import 'package:analyzer/dart/element/element.dart'; -// ignore: implementation_imports -import 'package:analyzer/src/dart/element/member.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:dartdoc/src/element_type.dart'; import 'package:dartdoc/src/model/attribute.dart'; import 'package:dartdoc/src/model/class.dart'; @@ -54,24 +53,25 @@ final class Annotation extends Attribute { /// The linked type argument text, with `<` and `>`, if there are any type /// arguments. String get _linkedTypeArguments { - var element = _annotation.element; - if (element is! SubstitutedConstructorElementImpl) { + if (_annotation.element is PropertyAccessorElement) { + return ''; + } + + var type = _modelType.type; + if (type is! InterfaceType) { + return ''; + } + + var typeArguments = type.typeArguments; + if (typeArguments.isEmpty) { return ''; } var buffer = StringBuffer(); buffer.write('<'); - var container = element.baseElement.enclosingElement; - for (var p in container.typeParameters) { - var type = element.substitution.map[p]; - assert(type != null); - if (type == null) { - // Abandon this type arguments string, as something is wrong with the - // user's code. - return ''; - } - buffer.write(_packageGraph.getTypeFor(type, _library).linkedName); - if (p != container.typeParameters.last) { + for (var t in typeArguments) { + buffer.write(_packageGraph.getTypeFor(t, _library).linkedName); + if (t != typeArguments.last) { buffer.write(', '); } }